You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/08/18 13:05:59 UTC

[01/24] incubator-brooklyn git commit: Test + fix BrooklynAccessUtils

Repository: incubator-brooklyn
Updated Branches:
  refs/heads/master 42d9871ec -> f092e183f


Test + fix BrooklynAccessUtils

- Previously there were no unit tests!
- Previously would not look up PortForwardManager unless machine
  implemented SupportsPortForwarding (which broke BYON’s tcpPortMappings)

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

Branch: refs/heads/master
Commit: ee1a20a04bacafba6633c3b7ec2e80635490310d
Parents: 6caee58
Author: Aled Sage <al...@gmail.com>
Authored: Mon Aug 17 12:32:38 2015 +0100
Committer: Aled Sage <al...@gmail.com>
Committed: Mon Aug 17 12:32:38 2015 +0100

----------------------------------------------------------------------
 .../location/access/BrooklynAccessUtils.java    |  52 ++++---
 .../access/BrooklynAccessUtilsTest.java         | 138 +++++++++++++++++++
 2 files changed, 170 insertions(+), 20 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ee1a20a0/core/src/main/java/org/apache/brooklyn/location/access/BrooklynAccessUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/location/access/BrooklynAccessUtils.java b/core/src/main/java/org/apache/brooklyn/location/access/BrooklynAccessUtils.java
index 9daf1c5..f1f858c 100644
--- a/core/src/main/java/org/apache/brooklyn/location/access/BrooklynAccessUtils.java
+++ b/core/src/main/java/org/apache/brooklyn/location/access/BrooklynAccessUtils.java
@@ -33,6 +33,8 @@ import brooklyn.event.basic.BasicConfigKey;
 import org.apache.brooklyn.location.basic.Machines;
 import org.apache.brooklyn.location.basic.SshMachineLocation;
 import org.apache.brooklyn.location.basic.SupportsPortForwarding;
+import org.python.google.common.base.Predicates;
+import org.python.google.common.collect.Iterables;
 
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.guava.Maybe;
@@ -66,27 +68,37 @@ public class BrooklynAccessUtils {
         PortForwardManager pfw = entity.getConfig(PORT_FORWARDING_MANAGER);
         if (pfw!=null) {
             Collection<Location> ll = entity.getLocations();
-            Maybe<SupportsPortForwarding> machine = Machines.findUniqueElement(ll, SupportsPortForwarding.class);
-            if (machine.isPresent()) {
-                synchronized (BrooklynAccessUtils.class) {
-                    // TODO finer-grained synchronization
-                    
-                    HostAndPort hp = pfw.lookup((MachineLocation)machine.get(), port);
-                    if (hp!=null) return hp;
-                    
-                    Location l = (Location) machine.get();
-                    if (l instanceof SupportsPortForwarding) {
-                        Cidr source = entity.getConfig(MANAGEMENT_ACCESS_CIDR);
-                        if (source!=null) {
-                            log.debug("BrooklynAccessUtils requesting new port-forwarding rule to access "+port+" on "+entity+" (at "+l+", enabled for "+source+")");
-                            // TODO discuss, is this the best way to do it
-                            // (will probably _create_ the port forwarding rule!)
-                            hp = ((SupportsPortForwarding) l).getSocketEndpointFor(source, port);
-                            if (hp!=null) return hp;
-                        } else {
-                            log.warn("No "+MANAGEMENT_ACCESS_CIDR.getName()+" configured for "+entity+", so cannot forward port "+port+" " +
-                                    "even though "+PORT_FORWARDING_MANAGER.getName()+" was supplied");
+            
+            synchronized (BrooklynAccessUtils.class) {
+                // TODO finer-grained synchronization
+                
+                for (MachineLocation machine : Iterables.filter(ll, MachineLocation.class)) {
+                    HostAndPort hp = pfw.lookup(machine, port);
+                    if (hp!=null) {
+                        log.debug("BrooklynAccessUtils found port-forwarded address {} for entity {}, port {}, using machine {}",
+                                new Object[] {hp, entity, port, machine});
+                        return hp;
+                    }
+                }
+                
+                Maybe<SupportsPortForwarding> supportPortForwardingLoc = Machines.findUniqueElement(ll, SupportsPortForwarding.class);
+                if (supportPortForwardingLoc.isPresent()) {
+                    Cidr source = entity.getConfig(MANAGEMENT_ACCESS_CIDR);
+                    SupportsPortForwarding loc = supportPortForwardingLoc.get();
+                    if (source!=null) {
+                        log.debug("BrooklynAccessUtils requesting new port-forwarding rule to access "+port+" on "+entity+" (at "+loc+", enabled for "+source+")");
+                        // TODO discuss, is this the best way to do it
+                        // (will probably _create_ the port forwarding rule!)
+                        HostAndPort hp = loc.getSocketEndpointFor(source, port);
+                        if (hp!=null) {
+                            log.debug("BrooklynAccessUtils created port-forwarded address {} for entity {}, port {}, using {}",
+                                    new Object[] {hp, entity, port, loc});
+                            return hp;
                         }
+                    } else {
+                        log.warn("No "+MANAGEMENT_ACCESS_CIDR.getName()+" configured for "+entity+", so cannot forward "
+                                +"port "+port+" "+"even though "+PORT_FORWARDING_MANAGER.getName()+" was supplied, and "
+                                +"have location supporting port forwarding "+loc);
                     }
                 }
             }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ee1a20a0/core/src/test/java/org/apache/brooklyn/location/access/BrooklynAccessUtilsTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/location/access/BrooklynAccessUtilsTest.java b/core/src/test/java/org/apache/brooklyn/location/access/BrooklynAccessUtilsTest.java
new file mode 100644
index 0000000..e5a8574
--- /dev/null
+++ b/core/src/test/java/org/apache/brooklyn/location/access/BrooklynAccessUtilsTest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.access;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.fail;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+import org.apache.brooklyn.location.basic.SupportsPortForwarding;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.net.HostAndPort;
+
+import brooklyn.entity.BrooklynAppUnitTestSupport;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.util.net.Cidr;
+
+public class BrooklynAccessUtilsTest extends BrooklynAppUnitTestSupport {
+
+    protected PortForwardManager pfm;
+    private TestEntity entity;
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        
+        pfm = (PortForwardManager) mgmt.getLocationRegistry().resolve("portForwardManager(scope=global)");
+    }
+    
+    @Test
+    public void testBrooklynAccessibleAddressFindsPreexistingMapping() throws Exception {
+        final int privatePort = 8080;
+        final String publicNatIp = "1.2.3.4";
+        final int publicNatPort = 12000;
+        
+        SshMachineLocation machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
+                .configure(SshMachineLocation.TCP_PORT_MAPPINGS, ImmutableMap.of(privatePort, publicNatIp+":"+publicNatPort)));
+        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class)
+                .configure(BrooklynAccessUtils.PORT_FORWARDING_MANAGER, pfm)
+                .location(machine));
+
+        assertEquals(BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, privatePort), HostAndPort.fromParts(publicNatIp, publicNatPort));
+    }
+    
+    @Test
+    public void testBrooklynAccessibleAddressUsesPrivateHostPortIfNoMapping() throws Exception {
+        final String privateIp = "127.1.2.3";
+        final int privatePort = 8080;
+        
+        SshMachineLocation machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class));
+        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class)
+                .configure(BrooklynAccessUtils.PORT_FORWARDING_MANAGER, pfm)
+                .location(machine));
+        entity.setAttribute(Attributes.HOSTNAME, privateIp);
+
+        assertEquals(BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, privatePort), HostAndPort.fromParts(privateIp, privatePort));
+    }
+    
+    @Test
+    public void testBrooklynAccessibleAddressFailsIfNoMappingAndNoHostname() throws Exception {
+        final int privatePort = 8080;
+        
+        SshMachineLocation machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class));
+        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class)
+                .configure(BrooklynAccessUtils.PORT_FORWARDING_MANAGER, pfm)
+                .location(machine));
+
+        try {
+            BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, privatePort);
+            fail();
+        } catch (IllegalStateException e) {
+            if (!e.toString().contains("no host.name")) throw e;
+            // success
+        }
+    }
+    
+    @Test
+    public void testBrooklynAccessibleAddressRequestsNewPortForwarding() throws Exception {
+        final int privatePort = 8080;
+        
+        RecordingSupportsPortForwarding machine = mgmt.getLocationManager().createLocation(LocationSpec.create(RecordingSupportsPortForwarding.class));
+        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class)
+                .configure(BrooklynAccessUtils.PORT_FORWARDING_MANAGER, pfm)
+                .configure(BrooklynAccessUtils.MANAGEMENT_ACCESS_CIDR, Cidr.UNIVERSAL)
+                .location(machine));
+
+        HostAndPort result = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, privatePort);
+        HostAndPort portForwarded = machine.mappings.get(privatePort);
+        assertNotNull(portForwarded);
+        assertEquals(result, portForwarded);
+    }
+    
+    @SuppressWarnings("serial") // TODO location should not be serializable; will be changed in future release
+    public static class RecordingSupportsPortForwarding extends SshMachineLocation implements SupportsPortForwarding {
+        protected final String publicIp = "1.2.3.4";
+        protected final Map<Integer, HostAndPort> mappings = Maps.newConcurrentMap();
+        private final AtomicInteger nextPort = new AtomicInteger(12000);
+        
+        
+        @Override
+        public HostAndPort getSocketEndpointFor(Cidr accessor, int privatePort) {
+            HostAndPort result = mappings.get(privatePort);
+            if (result == null) {
+                int publicPort = nextPort.getAndIncrement();
+                result = HostAndPort.fromParts(publicIp, publicPort);
+                mappings.put(privatePort, result);
+            }
+            return result;
+        }
+    }
+}


[11/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunPolicySoakTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunPolicySoakTest.java b/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunPolicySoakTest.java
deleted file mode 100644
index a67e24d..0000000
--- a/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunPolicySoakTest.java
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import static org.testng.Assert.assertEquals;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.location.Location;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-
-import org.apache.brooklyn.location.basic.SimulatedLocation;
-
-import brooklyn.policy.loadbalancing.BalanceableContainer;
-import brooklyn.policy.loadbalancing.MockContainerEntity;
-import brooklyn.policy.loadbalancing.MockItemEntity;
-import brooklyn.policy.loadbalancing.MockItemEntityImpl;
-import brooklyn.policy.loadbalancing.Movable;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-
-import com.google.common.base.Function;
-import com.google.common.base.Predicates;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
-
-public class FollowTheSunPolicySoakTest extends AbstractFollowTheSunPolicyTest {
-
-    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunPolicySoakTest.class);
-    
-    private static final long TIMEOUT_MS = 10*1000;
-    
-    @Test
-    public void testFollowTheSunQuickTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 1;
-        config.numLocations=3;
-        config.numContainersPerLocation = 5;
-        config.numLockedItemsPerLocation = 2;
-        config.numMovableItems = 10;
-    
-        runFollowTheSunSoakTest(config);
-    }
-    
-    @Test
-    public void testLoadBalancingManyItemsQuickTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 1;
-        config.numLocations=2;
-        config.numContainersPerLocation = 3;
-        config.numLockedItemsPerLocation = 2;
-        config.numMovableItems = 10;
-        config.numContainerStopsPerCycle = 1;
-        config.numItemStopsPerCycle = 1;
-    
-        runFollowTheSunSoakTest(config);
-    }
-    
-    @Test(groups={"Integration"}) // takes ~2s
-    public void testLoadBalancingManyItemsNotTooLongTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 1;
-        config.numLocations=3;
-        config.numContainersPerLocation = 5;
-        config.numLockedItemsPerLocation = 2;
-        config.numMovableItems = 500;
-        config.numContainerStopsPerCycle = 1;
-        config.numItemStopsPerCycle = 1;
-    
-        runFollowTheSunSoakTest(config);
-    }
-    
-    @Test(groups={"Integration","Acceptance"}) // integration group, because it's slow to run many cycles
-    public void testLoadBalancingSoakTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 100;
-        config.numLocations=3;
-        config.numContainersPerLocation = 5;
-        config.numLockedItemsPerLocation = 2;
-        config.numMovableItems = 10;
-    
-        runFollowTheSunSoakTest(config);
-    }
-
-    @Test(groups={"Integration","Acceptance"}) // integration group, because it's slow to run many cycles
-    public void testLoadBalancingManyItemsSoakTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 100;
-        config.numLocations=3;
-        config.numContainersPerLocation = 5;
-        config.numLockedItemsPerLocation = 2;
-        config.numMovableItems = 100;
-        config.numContainerStopsPerCycle = 3;
-        config.numItemStopsPerCycle = 10;
-        
-        runFollowTheSunSoakTest(config);
-    }
-
-    @Test(groups={"Integration","Acceptance"}) // integration group, because it's slow to run many cycles
-    public void testLoadBalancingManyManyItemsTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 1;
-        config.numLocations=10;
-        config.numContainersPerLocation = 5;
-        config.numLockedItemsPerLocation = 100;
-        config.numMovableItems = 1000;
-        config.numContainerStopsPerCycle = 0;
-        config.numItemStopsPerCycle = 0;
-        config.timeout_ms = 30*1000;
-        config.verbose = false;
-        
-        runFollowTheSunSoakTest(config);
-    }
-    
-    private void runFollowTheSunSoakTest(RunConfig config) {
-        int numCycles = config.numCycles;
-        int numLocations = config.numLocations;
-        int numContainersPerLocation = config.numContainersPerLocation;
-        int numLockedItemsPerLocation = config.numLockedItemsPerLocation;
-        int numMovableItems = config.numMovableItems;
-        
-        int numContainerStopsPerCycle = config.numContainerStopsPerCycle;
-        int numItemStopsPerCycle = config.numItemStopsPerCycle;
-        long timeout_ms = config.timeout_ms;
-        final boolean verbose = config.verbose;
-        
-        MockItemEntityImpl.totalMoveCount.set(0);
-        
-        List<Location> locations = new ArrayList<Location>();
-        Multimap<Location,MockContainerEntity> containers = HashMultimap.<Location,MockContainerEntity>create();
-        Multimap<Location,MockItemEntity> lockedItems = HashMultimap.<Location,MockItemEntity>create();
-        final List<MockItemEntity> movableItems = new ArrayList<MockItemEntity>();
-        
-        for (int i = 1; i <= numLocations; i++) {
-            String locName = "loc"+i;
-            Location loc = new SimulatedLocation(MutableMap.of("name",locName));
-            locations.add(loc);
-            
-            for (int j = 1; j <= numContainersPerLocation; j++) {
-                MockContainerEntity container = newContainer(app, loc, "container-"+locName+"-"+j);
-                containers.put(loc, container);
-            }
-            for (int j = 1; j <= numLockedItemsPerLocation; j++) {
-                MockContainerEntity container = Iterables.get(containers.get(loc), j%numContainersPerLocation);
-                MockItemEntity item = newLockedItem(app, container, "item-locked-"+locName+"-"+j);
-                lockedItems.put(loc, item);
-            }
-        }
-        
-        for (int i = 1; i <= numMovableItems; i++) {
-            MockContainerEntity container = Iterables.get(containers.values(), i%containers.size());
-            MockItemEntity item = newItem(app, container, "item-movable"+i);
-            movableItems.add(item);
-        }
-
-        for (int i = 1; i <= numCycles; i++) {
-            LOG.info("{}: cycle {}", FollowTheSunPolicySoakTest.class.getSimpleName(), i);
-            
-            // Stop movable items, and start others
-            for (int j = 1; j <= numItemStopsPerCycle; j++) {
-                int itemIndex = random.nextInt(numMovableItems);
-                MockItemEntity itemToStop = movableItems.get(itemIndex);
-                itemToStop.stop();
-                LOG.debug("Unmanaging item {}", itemToStop);
-                Entities.unmanage(itemToStop);
-                movableItems.set(itemIndex, newItem(app, Iterables.get(containers.values(), 0), "item-movable"+itemIndex));
-            }
-
-            // Choose a location to be busiest
-            int locIndex = random.nextInt(numLocations);
-            final Location busiestLocation = locations.get(locIndex);
-            
-            // Repartition the load across the items
-            for (int j = 0; j < numMovableItems; j++) {
-                MockItemEntity item = movableItems.get(j);
-                Map<Entity, Double> workrates = Maps.newLinkedHashMap();
-                
-                for (Map.Entry<Location,MockItemEntity> entry : lockedItems.entries()) {
-                    Location location = entry.getKey();
-                    MockItemEntity source = entry.getValue();
-                    double baseWorkrate = (location == busiestLocation ? 1000 : 100);
-                    double jitter = 10;
-                    double jitteredWorkrate = Math.max(0, baseWorkrate + (random.nextDouble()*jitter*2 - jitter));
-                    workrates.put(source, jitteredWorkrate);
-                }
-                ((EntityLocal)item).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, workrates);
-            }
-
-            // Stop containers, and start others
-            // This offloads the "immovable" items to other containers in the same location!
-            for (int j = 1; j <= numContainerStopsPerCycle; j++) {
-                int containerIndex = random.nextInt(containers.size());
-                MockContainerEntity containerToStop = Iterables.get(containers.values(), containerIndex);
-                Location location = Iterables.get(containerToStop.getLocations(), 0);
-                MockContainerEntity otherContainerInLocation = Iterables.find(containers.get(location), Predicates.not(Predicates.equalTo(containerToStop)), null);
-                containerToStop.offloadAndStop(otherContainerInLocation);
-                LOG.debug("Unmanaging container {}", containerToStop);
-                Entities.unmanage(containerToStop);
-                containers.remove(location, containerToStop);
-                
-                MockContainerEntity containerToAdd = newContainer(app, location, "container-"+location.getDisplayName()+"-new."+i+"."+j);
-                containers.put(location, containerToAdd);
-            }
-
-            // Assert that the items all end up in the location with maximum load-generation
-            Asserts.succeedsEventually(MutableMap.of("timeout", timeout_ms), new Runnable() {
-                public void run() {
-                    Iterable<Location> itemLocs = Iterables.transform(movableItems, new Function<MockItemEntity, Location>() {
-                        public Location apply(MockItemEntity input) {
-                            BalanceableContainer<?> container = input.getAttribute(Movable.CONTAINER);
-                            Collection<Location> locs = (container != null) ? container.getLocations(): null;
-                            return (locs != null && locs.size() > 0) ? Iterables.get(locs, 0) : null;
-                        }});
-                    
-                    Iterable<String> itemLocNames = Iterables.transform(itemLocs, new Function<Location, String>() {
-                        public String apply(Location input) {
-                            return (input != null) ? input.getDisplayName() : null;
-                        }});
-                    String errMsg;
-                    if (verbose) {
-                        errMsg = verboseDumpToString()+"; itemLocs="+itemLocNames;
-                    } else {
-                        Set<String> locNamesInUse = Sets.newLinkedHashSet(itemLocNames);
-                        errMsg = "locsInUse="+locNamesInUse+"; totalMoves="+MockItemEntityImpl.totalMoveCount;
-                    }
-                    
-                    assertEquals(ImmutableList.copyOf(itemLocs), Collections.nCopies(movableItems.size(), busiestLocation), errMsg);
-                }});
-        }
-    }
-    
-    static class RunConfig {
-        int numCycles = 1;
-        int numLocations = 3;
-        int numContainersPerLocation = 5;
-        int numLockedItemsPerLocation = 5;
-        int numMovableItems = 5;
-        int numContainerStopsPerCycle = 0;
-        int numItemStopsPerCycle = 0;
-        long timeout_ms = TIMEOUT_MS;
-        boolean verbose = true;
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunPolicyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunPolicyTest.java b/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunPolicyTest.java
deleted file mode 100644
index 1f5e5db..0000000
--- a/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunPolicyTest.java
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
-import static org.testng.Assert.fail;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.api.location.Location;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-
-import org.apache.brooklyn.location.basic.SimulatedLocation;
-
-import brooklyn.policy.loadbalancing.MockContainerEntity;
-import brooklyn.policy.loadbalancing.MockItemEntity;
-import brooklyn.policy.loadbalancing.Movable;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-
-import com.google.common.base.Function;
-import com.google.common.base.Stopwatch;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
-
-public class FollowTheSunPolicyTest extends AbstractFollowTheSunPolicyTest {
-    
-    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunPolicyTest.class);
-
-    @Test
-    public void testPolicyUpdatesModel() {
-        final MockContainerEntity containerA = newContainer(app, loc1, "A");
-        final MockItemEntity item1 = newItem(app, containerA, "1");
-        final MockItemEntity item2 = newItem(app, containerA, "2");
-        ((EntityLocal)item2).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item1, 11d));
-        
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            @Override public void run() {
-                assertEquals(ImmutableSet.of(item1, item2), model.getItems());
-                assertEquals(model.getItemContainer(item1), containerA);
-                assertEquals(model.getItemLocation(item1), loc1);
-                assertEquals(model.getContainerLocation(containerA), loc1);
-                assertEquals(model.getDirectSendsToItemByLocation(), ImmutableMap.of(item2, ImmutableMap.of(loc1, 11d)));
-            }});
-    }
-    
-    @Test
-    public void testPolicyAcceptsLocationFinder() {
-        pool.removePolicy(policy);
-        
-        Function<Entity, Location> customLocationFinder = new Function<Entity, Location>() {
-            @Override public Location apply(Entity input) {
-                return new SimulatedLocation(MutableMap.of("name", "custom location for "+input));
-            }};
-        
-        FollowTheSunPolicy customPolicy = new FollowTheSunPolicy(
-                MutableMap.of("minPeriodBetweenExecs", 0, "locationFinder", customLocationFinder), 
-                MockItemEntity.ITEM_USAGE_METRIC, 
-                model, 
-                FollowTheSunParameters.newDefault());
-        
-        pool.addPolicy(customPolicy);
-        
-        final MockContainerEntity containerA = newContainer(app, loc1, "A");
-        
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            @Override public void run() {
-                assertEquals(model.getContainerLocation(containerA).getDisplayName(), "custom location for "+containerA);
-            }});
-    }
-    
-    @Test
-    public void testNoopBalancing() throws Exception {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, loc1, "A");
-        MockContainerEntity containerB = newContainer(app, loc2, "B");
-        MockItemEntity item1 = newItem(app, containerA, "1", Collections.<Entity, Double>emptyMap());
-        MockItemEntity item2 = newItem(app, containerB, "2", Collections.<Entity, Double>emptyMap());
-        
-        Thread.sleep(SHORT_WAIT_MS);
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
-    }
-    
-    @Test
-    public void testMovesItemToFollowDemand() throws Exception {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, loc1, "A");
-        MockContainerEntity containerB = newContainer(app, loc2, "B");
-        MockItemEntity item1 = newItem(app, containerA, "1");
-        MockItemEntity item2 = newItem(app, containerB, "2");
-
-        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
-        
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.<MockItemEntity>of(), containerB, ImmutableList.of(item1, item2)));
-    }
-    
-    @Test
-    public void testNoopIfDemandIsTiny() throws Exception {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, loc1, "A");
-        MockContainerEntity containerB = newContainer(app, loc2, "B");
-        MockItemEntity item1 = newItem(app, containerA, "1");
-        MockItemEntity item2 = newItem(app, containerB, "2");
-
-        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 0.1d));
-        
-        Thread.sleep(SHORT_WAIT_MS);
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
-    }
-    
-    @Test
-    public void testNoopIfDemandIsSimilarToCurrentLocation() throws Exception {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, loc1, "A");
-        MockContainerEntity containerB = newContainer(app, loc2, "B");
-        MockItemEntity item1 = newItem(app, containerA, "1");
-        MockItemEntity item2 = newItem(app, containerA, "2");
-        MockItemEntity item3 = newItem(app, containerB, "3");
-        
-        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d, item3, 100.1d));
-        
-        Thread.sleep(SHORT_WAIT_MS);
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2), containerB, ImmutableList.of(item3)));
-    }
-    
-    @Test
-    public void testMoveDecisionIgnoresDemandFromItself() throws Exception {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, loc1, "A");
-        MockContainerEntity containerB = newContainer(app, loc2, "B");
-        MockItemEntity item1 = newItem(app, containerA, "1");
-        MockItemEntity item2 = newItem(app, containerB, "2");
-        
-        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item1, 100d));
-        ((EntityLocal)item2).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
-        
-        Thread.sleep(SHORT_WAIT_MS);
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
-    }
-    
-    @Test
-    public void testItemRemovedCausesRecalculationOfOptimalLocation() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, loc1, "A");
-        MockContainerEntity containerB = newContainer(app, loc2, "B");
-        MockItemEntity item1 = newItem(app, containerA, "1");
-        MockItemEntity item2 = newItem(app, containerA, "2");
-        MockItemEntity item3 = newItem(app, containerB, "3");
-        
-        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d, item3, 1000d));
-        
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item2), containerB, ImmutableList.of(item1, item3)));
-        
-        item3.stop();
-        Entities.unmanage(item3);
-        
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2), containerB, ImmutableList.<MockItemEntity>of()));
-    }
-    
-    @Test
-    public void testItemMovedCausesRecalculationOfOptimalLocationForOtherItems() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, loc1, "A");
-        MockContainerEntity containerB = newContainer(app, loc2, "B");
-        MockItemEntity item1 = newItem(app, containerA, "1");
-        MockItemEntity item2 = newItem(app, containerA, "2");
-        MockItemEntity item3 = newItem(app, containerB, "3");
-        
-        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d, item3, 1000d));
-        
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item2), containerB, ImmutableList.of(item1, item3)));
-        
-        item3.move(containerA);
-        
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2, item3), containerB, ImmutableList.<MockItemEntity>of()));
-    }
-    
-    @Test
-    public void testImmovableItemIsNotMoved() {
-        MockContainerEntity containerA = newContainer(app, loc1, "A");
-        MockContainerEntity containerB = newContainer(app, loc2, "B");
-        MockItemEntity item1 = newLockedItem(app, containerA, "1");
-        MockItemEntity item2 = newItem(app, containerB, "2");
-        
-        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
-        
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
-    }
-    
-    @Test
-    public void testImmovableItemContributesTowardsLoad() {
-        MockContainerEntity containerA = newContainer(app, loc1, "A");
-        MockContainerEntity containerB = newContainer(app, loc2, "B");
-        MockItemEntity item1 = newLockedItem(app, containerA, "1");
-        MockItemEntity item2 = newItem(app, containerA, "2");
-        
-        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item1, 100d));
-        
-        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2), containerB, ImmutableList.<MockItemEntity>of()));
-    }
-
-    // Marked as "Acceptance" due to time-sensitive nature :-(
-    @Test(groups={"Integration", "Acceptance"}, invocationCount=20)
-    public void testRepeatedRespectsMinPeriodBetweenExecs() throws Exception {
-        testRespectsMinPeriodBetweenExecs();
-    }
-    
-    @Test(groups="Integration")
-    public void testRespectsMinPeriodBetweenExecs() throws Exception {
-        // Failed in jenkins several times, e.g. with event times [2647, 2655] and [1387, 2001].
-        // Aled's guess is that there was a delay notifying the listener the first time
-        // (which happens async), causing the listener to be notified in rapid 
-        // succession. The policy execs probably did happen with a 1000ms separation.
-        // 
-        // Therefore try up to three times to see if we get the desired separation. If the 
-        // minPeriodBetweenExecs wasn't being respected, we'd expect the default 100ms; this 
-        // test would therefore hardly ever pass.
-        final int MAX_ATTEMPTS = 3;
-
-        final long minPeriodBetweenExecs = 1000;
-        final long timePrecision = 250;
-        
-        pool.removePolicy(policy);
-        
-        MockContainerEntity containerA = newContainer(app, loc1, "A");
-        MockContainerEntity containerB = newContainer(app, loc2, "B");
-        MockItemEntity item1 = newItem(app, containerA, "1");
-        MockItemEntity item2 = newItem(app, containerB, "2");
-        MockItemEntity item3 = newItem(app, containerA, "3");
-        
-        FollowTheSunPolicy customPolicy = new FollowTheSunPolicy(
-            MutableMap.of("minPeriodBetweenExecs", minPeriodBetweenExecs),
-            MockItemEntity.ITEM_USAGE_METRIC,
-            model,
-            FollowTheSunParameters.newDefault());
-    
-        pool.addPolicy(customPolicy);
-        
-        // Record times that things are moved, by lisening to the container sensor being set
-        final Stopwatch stopwatch = Stopwatch.createStarted();
-        
-        final List<Long> eventTimes = Lists.newCopyOnWriteArrayList();
-        final Semaphore semaphore = new Semaphore(0);
-        
-        app.subscribe(item1, Movable.CONTAINER, new SensorEventListener<Entity>() {
-            @Override public void onEvent(SensorEvent<Entity> event) {
-                long eventTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
-                LOG.info("Received {} at {}", event, eventTime);
-                eventTimes.add(eventTime);
-                semaphore.release();
-            }});
-
-        String errmsg = "";
-        for (int i = 0; i < MAX_ATTEMPTS; i++) {
-            // Set the workrate, causing the policy to move item1 to item2's location, and wait for it to happen
-            ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
-            assertTrue(semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-            assertEquals(item1.getAttribute(Movable.CONTAINER), containerB);
-            
-            // now cause item1 to be moved to item3's location, and wait for it to happen
-            ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item3, 100d));
-            assertTrue(semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-            assertEquals(item1.getAttribute(Movable.CONTAINER), containerA);
-            
-            LOG.info("testRepeatedRespectsMinPeriodBetweenExecs event times: "+eventTimes);
-            assertEquals(eventTimes.size(), 2);
-            if (eventTimes.get(1) - eventTimes.get(0) > (minPeriodBetweenExecs-timePrecision)) {
-                return; // success
-            } else {
-                errmsg += eventTimes;
-                eventTimes.clear();
-            }
-        }
-        
-        fail("Event times never had sufficient gap: "+errmsg);
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/ha/ConnectionFailureDetectorTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/ha/ConnectionFailureDetectorTest.java b/policy/src/test/java/brooklyn/policy/ha/ConnectionFailureDetectorTest.java
deleted file mode 100644
index bb2a2ec..0000000
--- a/policy/src/test/java/brooklyn/policy/ha/ConnectionFailureDetectorTest.java
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertTrue;
-import static org.testng.Assert.fail;
-
-import java.io.IOException;
-import java.net.ServerSocket;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.api.management.ManagementContext;
-import org.apache.brooklyn.api.policy.PolicySpec;
-import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.ApplicationBuilder;
-import brooklyn.entity.basic.Entities;
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.time.Duration;
-
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.net.HostAndPort;
-
-public class ConnectionFailureDetectorTest {
-
-    private static final int TIMEOUT_MS = 30*1000;
-    private static final int OVERHEAD = 250;
-    private static final int POLL_PERIOD = 100;
-
-    private ManagementContext managementContext;
-    private TestApplication app;
-    
-    private List<SensorEvent<FailureDescriptor>> events;
-    
-    private ServerSocket serverSocket;
-    private HostAndPort serverSocketAddress;
-    
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        events = new CopyOnWriteArrayList<SensorEvent<FailureDescriptor>>();
-        
-        managementContext = new LocalManagementContextForTests();
-        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
-        
-        app.getManagementContext().getSubscriptionManager().subscribe(
-                app, 
-                HASensors.CONNECTION_FAILED, 
-                new SensorEventListener<FailureDescriptor>() {
-                    @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
-                        events.add(event);
-                    }
-                });
-        app.getManagementContext().getSubscriptionManager().subscribe(
-                app, 
-                HASensors.CONNECTION_RECOVERED, 
-                new SensorEventListener<FailureDescriptor>() {
-                    @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
-                        events.add(event);
-                    }
-                });
-        
-        serverSocketAddress = startServerSocket();
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        stopServerSocket();
-        if (managementContext != null) Entities.destroyAll(managementContext);
-    }
-    
-    private HostAndPort startServerSocket() throws Exception {
-        if (serverSocketAddress != null) {
-            serverSocket = new ServerSocket(serverSocketAddress.getPort());
-        } else {
-            for (int i = 40000; i < 40100; i++) {
-                try {
-                    serverSocket = new ServerSocket(i);
-                } catch (IOException e) {
-                    // try next port
-                }
-            }
-            assertNotNull(serverSocket, "Failed to create server socket; no ports free in range!");
-            serverSocketAddress = HostAndPort.fromParts(serverSocket.getInetAddress().getHostAddress(), serverSocket.getLocalPort());
-        }
-        return serverSocketAddress;
-    }
-
-    private void stopServerSocket() throws Exception {
-        if (serverSocket != null) serverSocket.close();
-    }
-
-    @Test(groups="Integration") // Has a 1 second wait
-    public void testNotNotifiedOfFailuresForHealthy() throws Exception {
-        // Create members before and after the policy is registered, to test both scenarios
-        
-        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
-                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress));
-        
-        assertNoEventsContinually();
-    }
-    
-    @Test
-    public void testNotifiedOfFailure() throws Exception {
-        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
-                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress));
-
-        stopServerSocket();
-
-        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
-        assertEquals(events.size(), 1, "events="+events);
-    }
-    
-    @Test
-    public void testNotifiedOfRecovery() throws Exception {
-        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
-                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress));
-        
-        stopServerSocket();
-        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
-
-        // make the connection recover
-        startServerSocket();
-        assertHasEventEventually(HASensors.CONNECTION_RECOVERED, Predicates.<Object>equalTo(app), null);
-        assertEquals(events.size(), 2, "events="+events);
-    }
-    
-    @Test
-    public void testReportsFailureWhenAlreadyDownOnRegisteringPolicy() throws Exception {
-        stopServerSocket();
-
-        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
-                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress));
-
-        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
-    }
-
-    @Test(groups="Integration") // Because slow
-    public void testNotNotifiedOfTemporaryFailuresDuringStabilisationDelay() throws Exception {
-        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
-                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
-                .configure(ConnectionFailureDetector.CONNECTION_FAILED_STABILIZATION_DELAY, Duration.ONE_MINUTE));
-        
-        stopServerSocket();
-        Thread.sleep(100);
-        startServerSocket();
-
-        assertNoEventsContinually();
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testNotifiedOfFailureAfterStabilisationDelay() throws Exception {
-        final int stabilisationDelay = 1000;
-        
-        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
-                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
-                .configure(ConnectionFailureDetector.CONNECTION_FAILED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
-        
-        stopServerSocket();
-
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testFailuresThenUpDownResetsStabilisationCount() throws Exception {
-        final long stabilisationDelay = 1000;
-        
-        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
-                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
-                .configure(ConnectionFailureDetector.CONNECTION_FAILED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
-        
-        stopServerSocket();
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-
-        startServerSocket();
-        Thread.sleep(POLL_PERIOD+OVERHEAD);
-        stopServerSocket();
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-        
-        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testNotNotifiedOfTemporaryRecoveryDuringStabilisationDelay() throws Exception {
-        final long stabilisationDelay = 1000;
-        
-        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
-                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
-                .configure(ConnectionFailureDetector.CONNECTION_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
-        
-        stopServerSocket();
-        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
-        events.clear();
-        
-        startServerSocket();
-        Thread.sleep(POLL_PERIOD+OVERHEAD);
-        stopServerSocket();
-
-        assertNoEventsContinually(Duration.of(stabilisationDelay + OVERHEAD));
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testNotifiedOfRecoveryAfterStabilisationDelay() throws Exception {
-        final int stabilisationDelay = 1000;
-        
-        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
-                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
-                .configure(ConnectionFailureDetector.CONNECTION_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
-        
-        stopServerSocket();
-        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
-        events.clear();
-
-        startServerSocket();
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-        assertHasEventEventually(HASensors.CONNECTION_RECOVERED, Predicates.<Object>equalTo(app), null);
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testRecoversThenDownUpResetsStabilisationCount() throws Exception {
-        final long stabilisationDelay = 1000;
-        
-        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
-                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
-                .configure(ConnectionFailureDetector.CONNECTION_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
-        
-        stopServerSocket();
-        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
-        events.clear();
-        
-        startServerSocket();
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-        
-        stopServerSocket();
-        Thread.sleep(POLL_PERIOD+OVERHEAD);
-        startServerSocket();
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-
-        assertHasEventEventually(HASensors.CONNECTION_RECOVERED, Predicates.<Object>equalTo(app), null);
-    }
-
-    private void assertHasEvent(Sensor<?> sensor, Predicate<Object> componentPredicate, Predicate<? super CharSequence> descriptionPredicate) {
-        for (SensorEvent<FailureDescriptor> event : events) {
-            if (event.getSensor().equals(sensor) && 
-                    (componentPredicate == null || componentPredicate.apply(event.getValue().getComponent())) &&
-                    (descriptionPredicate == null || descriptionPredicate.apply(event.getValue().getDescription()))) {
-                return;
-            }
-        }
-        fail("No matching "+sensor+" event found; events="+events);
-    }
-    
-    private void assertHasEventEventually(final Sensor<?> sensor, final Predicate<Object> componentPredicate, final Predicate<? super CharSequence> descriptionPredicate) {
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            @Override public void run() {
-                assertHasEvent(sensor, componentPredicate, descriptionPredicate);
-            }});
-    }
-    
-    private void assertNoEventsContinually(Duration duration) {
-        Asserts.succeedsContinually(ImmutableMap.of("timeout", duration), new Runnable() {
-            @Override public void run() {
-                assertTrue(events.isEmpty(), "events="+events);
-            }});
-    }
-    
-    private void assertNoEventsContinually() {
-        Asserts.succeedsContinually(new Runnable() {
-            @Override public void run() {
-                assertTrue(events.isEmpty(), "events="+events);
-            }});
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/ha/HaPolicyRebindTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/ha/HaPolicyRebindTest.java b/policy/src/test/java/brooklyn/policy/ha/HaPolicyRebindTest.java
deleted file mode 100644
index e6d01d0..0000000
--- a/policy/src/test/java/brooklyn/policy/ha/HaPolicyRebindTest.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.fail;
-
-import java.util.List;
-import java.util.Set;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.api.location.LocationSpec;
-import org.apache.brooklyn.api.policy.EnricherSpec;
-import org.apache.brooklyn.api.policy.PolicySpec;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.apache.brooklyn.test.entity.TestEntity;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.basic.Lifecycle;
-import brooklyn.entity.basic.ServiceStateLogic;
-import brooklyn.entity.group.DynamicCluster;
-import brooklyn.entity.rebind.RebindTestFixtureWithApp;
-
-import org.apache.brooklyn.location.basic.SimulatedLocation;
-
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-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.collect.Sets;
-
-public class HaPolicyRebindTest extends RebindTestFixtureWithApp {
-
-    private TestEntity origEntity;
-    private SensorEventListener<FailureDescriptor> eventListener;
-    private List<SensorEvent<FailureDescriptor>> events;
-    
-    @Override
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        super.setUp();
-        origEntity = origApp.createAndManageChild(EntitySpec.create(TestEntity.class));
-        events = Lists.newCopyOnWriteArrayList();
-        eventListener = new SensorEventListener<FailureDescriptor>() {
-            @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
-                events.add(event);
-            }
-        };
-    }
-
-    @Test
-    public void testServiceRestarterWorksAfterRebind() throws Exception {
-        origEntity.addPolicy(PolicySpec.create(ServiceRestarter.class)
-                .configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
-        
-        TestApplication newApp = rebind();
-        final TestEntity newEntity = (TestEntity) Iterables.find(newApp.getChildren(), Predicates.instanceOf(TestEntity.class));
-        
-        newEntity.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(origEntity, "simulate failure"));
-        
-        Asserts.succeedsEventually(new Runnable() {
-            @Override public void run() {
-                assertEquals(newEntity.getCallHistory(), ImmutableList.of("restart"));
-            }});
-    }
-
-    @Test
-    public void testServiceReplacerWorksAfterRebind() throws Exception {
-        Location origLoc = origManagementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
-        DynamicCluster origCluster = origApp.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(TestEntity.class))
-                .configure(DynamicCluster.INITIAL_SIZE, 3));
-        origApp.start(ImmutableList.<Location>of(origLoc));
-
-        origCluster.addPolicy(PolicySpec.create(ServiceReplacer.class)
-                .configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
-
-        // rebind
-        TestApplication newApp = rebind();
-        final DynamicCluster newCluster = (DynamicCluster) Iterables.find(newApp.getChildren(), Predicates.instanceOf(DynamicCluster.class));
-
-        // stimulate the policy
-        final Set<Entity> initialMembers = ImmutableSet.copyOf(newCluster.getMembers());
-        final TestEntity e1 = (TestEntity) Iterables.get(initialMembers, 1);
-        
-        newApp.getManagementContext().getSubscriptionManager().subscribe(e1, HASensors.ENTITY_FAILED, eventListener);
-        newApp.getManagementContext().getSubscriptionManager().subscribe(e1, HASensors.ENTITY_RECOVERED, eventListener);
-        
-        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
-        
-        // Expect e1 to be replaced
-        Asserts.succeedsEventually(new Runnable() {
-            @Override public void run() {
-                Set<Entity> newMembers = Sets.difference(ImmutableSet.copyOf(newCluster.getMembers()), initialMembers);
-                Set<Entity> removedMembers = Sets.difference(initialMembers, ImmutableSet.copyOf(newCluster.getMembers()));
-                assertEquals(removedMembers, ImmutableSet.of(e1));
-                assertEquals(newMembers.size(), 1);
-                assertEquals(((TestEntity)Iterables.getOnlyElement(newMembers)).getCallHistory(), ImmutableList.of("start"));
-                
-                // TODO e1 not reporting "start" after rebind because callHistory is a field rather than an attribute, so was not persisted
-                Asserts.assertEqualsIgnoringOrder(e1.getCallHistory(), ImmutableList.of("stop"));
-                assertFalse(Entities.isManaged(e1));
-            }});
-    }
-    
-    @Test
-    public void testServiceFailureDetectorWorksAfterRebind() throws Exception {
-        origEntity.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
-
-        // rebind
-        TestApplication newApp = rebind();
-        final TestEntity newEntity = (TestEntity) Iterables.find(newApp.getChildren(), Predicates.instanceOf(TestEntity.class));
-
-        newApp.getManagementContext().getSubscriptionManager().subscribe(newEntity, HASensors.ENTITY_FAILED, eventListener);
-
-        newEntity.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(newEntity, Lifecycle.RUNNING);
-        
-        // trigger the failure
-        newEntity.setAttribute(TestEntity.SERVICE_UP, false);
-
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(newEntity), null);
-        assertEquals(events.size(), 1, "events="+events);
-    }
-    
-    private void assertHasEventEventually(final Sensor<?> sensor, final Predicate<Object> componentPredicate, final Predicate<? super CharSequence> descriptionPredicate) {
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            @Override public void run() {
-                assertHasEvent(sensor, componentPredicate, descriptionPredicate);
-            }});
-    }
-    
-    private void assertHasEvent(Sensor<?> sensor, Predicate<Object> componentPredicate, Predicate<? super CharSequence> descriptionPredicate) {
-        for (SensorEvent<FailureDescriptor> event : events) {
-            if (event.getSensor().equals(sensor) && 
-                    (componentPredicate == null || componentPredicate.apply(event.getValue().getComponent())) &&
-                    (descriptionPredicate == null || descriptionPredicate.apply(event.getValue().getDescription()))) {
-                return;
-            }
-        }
-        fail("No matching "+sensor+" event found; events="+events);
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/ha/ServiceFailureDetectorStabilizationTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/ha/ServiceFailureDetectorStabilizationTest.java b/policy/src/test/java/brooklyn/policy/ha/ServiceFailureDetectorStabilizationTest.java
deleted file mode 100644
index b6c5c7b..0000000
--- a/policy/src/test/java/brooklyn/policy/ha/ServiceFailureDetectorStabilizationTest.java
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import static org.testng.Assert.assertTrue;
-import static org.testng.Assert.fail;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.api.management.ManagementContext;
-import org.apache.brooklyn.api.policy.EnricherSpec;
-import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.apache.brooklyn.test.entity.TestEntity;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.ApplicationBuilder;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.basic.Lifecycle;
-import brooklyn.entity.basic.ServiceStateLogic;
-import brooklyn.entity.basic.ServiceStateLogicTest;
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.time.Duration;
-
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableMap;
-
-/** also see more primitive tests in {@link ServiceStateLogicTest} */
-public class ServiceFailureDetectorStabilizationTest {
-
-    private static final Logger LOG = LoggerFactory.getLogger(ServiceFailureDetectorStabilizationTest.class);
-
-    private static final int TIMEOUT_MS = 10*1000;
-    private static final int OVERHEAD = 250;
-
-    private ManagementContext managementContext;
-    private TestApplication app;
-    private TestEntity e1;
-    
-    private List<SensorEvent<FailureDescriptor>> events;
-    
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        events = new CopyOnWriteArrayList<SensorEvent<FailureDescriptor>>();
-        
-        managementContext = new LocalManagementContextForTests();
-        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
-        e1 = app.createAndManageChild(EntitySpec.create(TestEntity.class));
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        
-        app.getManagementContext().getSubscriptionManager().subscribe(
-                e1, 
-                HASensors.ENTITY_FAILED, 
-                new SensorEventListener<FailureDescriptor>() {
-                    @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
-                        events.add(event);
-                    }
-                });
-        app.getManagementContext().getSubscriptionManager().subscribe(
-                e1, 
-                HASensors.ENTITY_RECOVERED, 
-                new SensorEventListener<FailureDescriptor>() {
-                    @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
-                        events.add(event);
-                    }
-                });
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        if (managementContext != null) Entities.destroyAll(managementContext);
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testNotNotifiedOfTemporaryFailuresDuringStabilisationDelay() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-                .configure(ServiceFailureDetector.ENTITY_FAILED_STABILIZATION_DELAY, Duration.ONE_MINUTE));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-        Thread.sleep(100);
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-
-        assertNoEventsContinually();
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testNotifiedOfFailureAfterStabilisationDelay() throws Exception {
-        final int stabilisationDelay = 1000;
-        
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-                .configure(ServiceFailureDetector.ENTITY_FAILED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testFailuresThenUpDownResetsStabilisationCount() throws Exception {
-        LOG.debug("Running testFailuresThenUpDownResetsStabilisationCount");
-        final long stabilisationDelay = 1000;
-        
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-                .configure(ServiceFailureDetector.ENTITY_FAILED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        Thread.sleep(OVERHEAD);
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-        
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testNotNotifiedOfTemporaryRecoveryDuringStabilisationDelay() throws Exception {
-        final long stabilisationDelay = 1000;
-        
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-                .configure(ServiceFailureDetector.ENTITY_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-        events.clear();
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        Thread.sleep(100);
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-
-        assertNoEventsContinually(Duration.of(stabilisationDelay + OVERHEAD));
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testNotifiedOfRecoveryAfterStabilisationDelay() throws Exception {
-        final int stabilisationDelay = 1000;
-        
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-                .configure(ServiceFailureDetector.ENTITY_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-        events.clear();
-
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
-    }
-    
-    @Test(groups="Integration") // Because slow
-    public void testRecoversThenDownUpResetsStabilisationCount() throws Exception {
-        final long stabilisationDelay = 1000;
-        
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-                .configure(ServiceFailureDetector.ENTITY_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-        events.clear();
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-        Thread.sleep(OVERHEAD);
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
-
-        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
-    }
-
-    private void assertHasEvent(Sensor<?> sensor, Predicate<Object> componentPredicate, Predicate<? super CharSequence> descriptionPredicate) {
-        for (SensorEvent<FailureDescriptor> event : events) {
-            if (event.getSensor().equals(sensor) && 
-                    (componentPredicate == null || componentPredicate.apply(event.getValue().getComponent())) &&
-                    (descriptionPredicate == null || descriptionPredicate.apply(event.getValue().getDescription()))) {
-                return;
-            }
-        }
-        fail("No matching "+sensor+" event found; events="+events);
-    }
-    
-    private void assertHasEventEventually(final Sensor<?> sensor, final Predicate<Object> componentPredicate, final Predicate<? super CharSequence> descriptionPredicate) {
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            @Override public void run() {
-                assertHasEvent(sensor, componentPredicate, descriptionPredicate);
-            }});
-    }
-
-    private void assertNoEventsContinually(Duration duration) {
-        Asserts.succeedsContinually(ImmutableMap.of("timeout", duration), new Runnable() {
-            @Override public void run() {
-                assertTrue(events.isEmpty(), "events="+events);
-            }});
-    }
-    
-    private void assertNoEventsContinually() {
-        Asserts.succeedsContinually(new Runnable() {
-            @Override public void run() {
-                assertTrue(events.isEmpty(), "events="+events);
-            }});
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/ha/ServiceFailureDetectorTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/ha/ServiceFailureDetectorTest.java b/policy/src/test/java/brooklyn/policy/ha/ServiceFailureDetectorTest.java
deleted file mode 100644
index 37355cf..0000000
--- a/policy/src/test/java/brooklyn/policy/ha/ServiceFailureDetectorTest.java
+++ /dev/null
@@ -1,408 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
-import static org.testng.Assert.fail;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.api.management.ManagementContext;
-import org.apache.brooklyn.api.policy.EnricherSpec;
-import org.apache.brooklyn.test.EntityTestUtils;
-import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.apache.brooklyn.test.entity.TestEntity;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.ApplicationBuilder;
-import brooklyn.entity.basic.Attributes;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.basic.Lifecycle;
-import brooklyn.entity.basic.ServiceStateLogic;
-import brooklyn.entity.basic.ServiceStateLogic.ServiceProblemsLogic;
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.time.Duration;
-import brooklyn.util.time.Time;
-
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableMap;
-
-public class ServiceFailureDetectorTest {
-    private static final Logger log = LoggerFactory.getLogger(ServiceFailureDetectorTest.class);
-
-    private static final int TIMEOUT_MS = 10*1000;
-
-    private ManagementContext managementContext;
-    private TestApplication app;
-    private TestEntity e1;
-    
-    private List<SensorEvent<FailureDescriptor>> events;
-    private SensorEventListener<FailureDescriptor> eventListener;
-    
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        events = new CopyOnWriteArrayList<SensorEvent<FailureDescriptor>>();
-        eventListener = new SensorEventListener<FailureDescriptor>() {
-            @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
-                events.add(event);
-            }
-        };
-        
-        managementContext = new LocalManagementContextForTests();
-        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
-        e1 = app.createAndManageChild(EntitySpec.create(TestEntity.class));
-        e1.addEnricher(ServiceStateLogic.newEnricherForServiceStateFromProblemsAndUp());
-        
-        app.getManagementContext().getSubscriptionManager().subscribe(e1, HASensors.ENTITY_FAILED, eventListener);
-        app.getManagementContext().getSubscriptionManager().subscribe(e1, HASensors.ENTITY_RECOVERED, eventListener);
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        if (managementContext != null) Entities.destroyAll(managementContext);
-    }
-    
-    @Test(groups="Integration") // Has a 1 second wait
-    public void testNotNotifiedOfFailuresForHealthy() throws Exception {
-        // Create members before and after the policy is registered, to test both scenarios
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
-        
-        assertNoEventsContinually();
-        assertEquals(e1.getAttribute(TestEntity.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING);
-    }
-    
-    @Test
-    public void testNotifiedOfFailure() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        
-        assertEquals(events.size(), 0, "events="+events);
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-        assertEquals(events.size(), 1, "events="+events);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-    }
-    
-    @Test
-    public void testNotifiedOfFailureOnProblem() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        
-        assertEquals(events.size(), 0, "events="+events);
-        
-        ServiceProblemsLogic.updateProblemsIndicator(e1, "test", "foo");
-
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-        assertEquals(events.size(), 1, "events="+events);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-    }
-    
-    @Test
-    public void testNotifiedOfFailureOnStateOnFire() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.ON_FIRE);
-
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-        assertEquals(events.size(), 1, "events="+events);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-    }
-    
-    @Test
-    public void testNotifiedOfRecovery() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        // Make the entity fail
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-
-        // And make the entity recover
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
-        assertEquals(events.size(), 2, "events="+events);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
-    }
-    
-    @Test
-    public void testNotifiedOfRecoveryFromProblems() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        // Make the entity fail
-        ServiceProblemsLogic.updateProblemsIndicator(e1, "test", "foo");
-
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-
-        // And make the entity recover
-        ServiceProblemsLogic.clearProblemsIndicator(e1, "test");
-        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
-        assertEquals(events.size(), 2, "events="+events);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
-    }
-    
-    
-    @Test(groups="Integration") // Has a 1 second wait
-    public void testEmitsEntityFailureOnlyIfPreviouslyUp() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
-        
-        // Make the entity fail
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-        assertNoEventsContinually();
-    }
-    
-    @Test
-    public void testDisablingPreviouslyUpRequirementForEntityFailed() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-            .configure(ServiceFailureDetector.ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP, false));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-    }
-    
-    @Test
-    public void testDisablingOnFire() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-            .configure(ServiceFailureDetector.SERVICE_ON_FIRE_STABILIZATION_DELAY, Duration.PRACTICALLY_FOREVER));
-        
-        // Make the entity fail
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-
-        assertEquals(e1.getAttribute(TestEntity.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING);
-    }
-    
-    @Test(groups="Integration") // Has a 1 second wait
-    public void testOnFireAfterDelay() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-            .configure(ServiceFailureDetector.SERVICE_ON_FIRE_STABILIZATION_DELAY, Duration.ONE_SECOND));
-        
-        // Make the entity fail
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-
-        assertEquals(e1.getAttribute(TestEntity.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING);
-        Time.sleep(Duration.millis(100));
-        assertEquals(e1.getAttribute(TestEntity.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-    }
-    
-    @Test(groups="Integration") // Has a 1 second wait
-    public void testOnFailureDelayFromProblemAndRecover() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-            .configure(ServiceFailureDetector.SERVICE_ON_FIRE_STABILIZATION_DELAY, Duration.ONE_SECOND)
-            .configure(ServiceFailureDetector.ENTITY_RECOVERED_STABILIZATION_DELAY, Duration.ONE_SECOND));
-        
-        // Set the entity to healthy
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
-        
-        // Make the entity fail; won't set on-fire for 1s but will publish FAILED immediately.
-        ServiceStateLogic.ServiceProblemsLogic.updateProblemsIndicator(e1, "test", "foo");
-        EntityTestUtils.assertAttributeEqualsContinually(ImmutableMap.of("timeout", 100), e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-        assertEquals(e1.getAttribute(TestEntity.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING);
-        
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-        
-        // Now recover: will publish RUNNING immediately, but has 1s stabilisation for RECOVERED
-        ServiceStateLogic.ServiceProblemsLogic.clearProblemsIndicator(e1, "test");
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
-        
-        assertEquals(events.size(), 1, "events="+events);
-        
-        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
-        assertEquals(events.size(), 2, "events="+events);
-    }
-    
-    @Test(groups="Integration") // Has a 1 second wait
-    public void testAttendsToServiceState() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
-        
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        // not counted as failed because not expected to be running
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-
-        assertNoEventsContinually();
-    }
-
-    @Test(groups="Integration") // Has a 1 second wait
-    public void testOnlyReportsFailureIfRunning() throws Exception {
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
-        
-        // Make the entity fail
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.STARTING);
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-
-        assertNoEventsContinually();
-    }
-    
-    @Test
-    public void testReportsFailureWhenAlreadyDownOnRegisteringPolicy() throws Exception {
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        e1.setAttribute(TestEntity.SERVICE_UP, false);
-
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-            .configure(ServiceFailureDetector.ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP, false));
-
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-    }
-    
-    @Test
-    public void testReportsFailureWhenAlreadyOnFireOnRegisteringPolicy() throws Exception {
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.ON_FIRE);
-
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-            .configure(ServiceFailureDetector.ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP, false));
-
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-    }
-    
-    @Test(groups="Integration") // Has a 1.5 second wait
-    public void testRepublishedFailure() throws Exception {
-        Duration republishPeriod = Duration.millis(100);
-
-        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
-                .configure(ServiceFailureDetector.ENTITY_FAILED_REPUBLISH_TIME, republishPeriod));
-            
-        // Set the entity to healthy
-        e1.setAttribute(TestEntity.SERVICE_UP, true);
-        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
-        EntityTestUtils.assertAttributeEqualsEventually(e1, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
-        
-        // Make the entity fail;
-        ServiceStateLogic.ServiceProblemsLogic.updateProblemsIndicator(e1, "test", "foo");
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
-
-        //wait for at least 10 republish events (~1 sec)
-        assertEventsSizeEventually(10);
-
-        // Now recover
-        ServiceStateLogic.ServiceProblemsLogic.clearProblemsIndicator(e1, "test");
-        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
-        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
-
-        //once recovered check no more failed events emitted periodically
-        assertEventsSizeContiniually(events.size());
-
-        SensorEvent<FailureDescriptor> prevEvent = null;
-        for (SensorEvent<FailureDescriptor> event : events) {
-            if (prevEvent != null) {
-                long repeatOffset = event.getTimestamp() - prevEvent.getTimestamp();
-                long deviation = Math.abs(repeatOffset - republishPeriod.toMilliseconds());
-                if (deviation > republishPeriod.toMilliseconds()/10 &&
-                        //warn only if recovered is too far away from the last failure
-                        (!event.getSensor().equals(HASensors.ENTITY_RECOVERED) ||
-                        repeatOffset > republishPeriod.toMilliseconds())) {
-                    log.error("The time between failure republish (" + repeatOffset + "ms) deviates too much from the expected " + republishPeriod + ". prevEvent=" + prevEvent + ", event=" + event);
-                }
-            }
-            prevEvent = event;
-        }
-        
-        //make sure no republish takes place after recovered
-        assertEquals(prevEvent.getSensor(), HASensors.ENTITY_RECOVERED);
-    }
-    
-    private void assertEventsSizeContiniually(final int size) {
-        Asserts.succeedsContinually(MutableMap.of("timeout", 500), new Runnable() {
-            @Override
-            public void run() {
-                assertTrue(events.size() == size, "assertEventsSizeContiniually expects " + size + " events but found " + events.size() + ": " + events);
-            }
-        });
-    }
-
-    private void assertEventsSizeEventually(final int size) {
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            @Override
-            public void run() {
-                assertTrue(events.size() >= size, "assertEventsSizeContiniually expects at least " + size + " events but found " + events.size() + ": " + events);
-            }
-        });
-    }
-
-    private void assertHasEvent(Sensor<?> sensor, Predicate<Object> componentPredicate, Predicate<? super CharSequence> descriptionPredicate) {
-        for (SensorEvent<FailureDescriptor> event : events) {
-            if (event.getSensor().equals(sensor) && 
-                    (componentPredicate == null || componentPredicate.apply(event.getValue().getComponent())) &&
-                    (descriptionPredicate == null || descriptionPredicate.apply(event.getValue().getDescription()))) {
-                return;
-            }
-        }
-        fail("No matching "+sensor+" event found; events="+events);
-    }
-    
-    private void assertHasEventEventually(final Sensor<?> sensor, final Predicate<Object> componentPredicate, final Predicate<? super CharSequence> descriptionPredicate) {
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            @Override public void run() {
-                assertHasEvent(sensor, componentPredicate, descriptionPredicate);
-            }});
-    }
-    
-    private void assertNoEventsContinually() {
-        Asserts.succeedsContinually(new Runnable() {
-            @Override public void run() {
-                assertTrue(events.isEmpty(), "events="+events);
-            }});
-    }
-}


[09/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicyTest.java b/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicyTest.java
deleted file mode 100644
index 442e3b3..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicyTest.java
+++ /dev/null
@@ -1,397 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-
-public class LoadBalancingPolicyTest extends AbstractLoadBalancingPolicyTest {
-    
-    // Expect no balancing to occur as container A isn't above the high threshold.
-    @Test
-    public void testNoopWhenWithinThresholds() {
-        MockContainerEntity containerA = newContainer(app, "A", 10, 100);
-        MockContainerEntity containerB = newContainer(app, "B", 20, 60);
-        MockItemEntity item1 = newItem(app, containerA, "1", 10);
-        MockItemEntity item2 = newItem(app, containerA, "2", 10);
-        MockItemEntity item3 = newItem(app, containerA, "3", 10);
-        MockItemEntity item4 = newItem(app, containerA, "4", 10);
-
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3, item4), 
-                ImmutableList.of(40d, 0d));
-    }
-    
-    @Test
-    public void testNoopWhenAlreadyBalanced() {
-        MockContainerEntity containerA = newContainer(app, "A", 20, 80);
-        MockContainerEntity containerB = newContainer(app, "B", 20, 80);
-        MockItemEntity item1 = newItem(app, containerA, "1", 10);
-        MockItemEntity item2 = newItem(app, containerA, "2", 30);
-        MockItemEntity item3 = newItem(app, containerB, "3", 20);
-        MockItemEntity item4 = newItem(app, containerB, "4", 20);
-
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3, item4), 
-                ImmutableList.of(40d, 40d));
-        assertEquals(containerA.getBalanceableItems(), ImmutableSet.of(item1, item2));
-        assertEquals(containerB.getBalanceableItems(), ImmutableSet.of(item3, item4));
-    }
-    
-    // Expect 20 units of workload to be migrated from hot container (A) to cold (B).
-    @Test
-    public void testSimpleBalancing() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 25);
-        MockContainerEntity containerB = newContainer(app, "B", 20, 60);
-        MockItemEntity item1 = newItem(app, containerA, "1", 10);
-        MockItemEntity item2 = newItem(app, containerA, "2", 10);
-        MockItemEntity item3 = newItem(app, containerA, "3", 10);
-        MockItemEntity item4 = newItem(app, containerA, "4", 10);
-
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3, item4), 
-                ImmutableList.of(20d, 20d));
-    }
-    
-    @Test
-    public void testSimpleBalancing2() {
-        MockContainerEntity containerA = newContainer(app, "A", 20, 40);
-        MockContainerEntity containerB = newContainer(app, "B", 20, 40);
-        MockItemEntity item1 = newItem(app, containerA, "1", 0);
-        MockItemEntity item2 = newItem(app, containerB, "2", 40);
-        MockItemEntity item3 = newItem(app, containerB, "3", 20);
-        MockItemEntity item4 = newItem(app, containerB, "4", 20);
-
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3, item4), 
-                ImmutableList.of(40d, 40d));
-    }
-    
-//    @Test
-//    public void testAdjustedItemNotMoved() {
-//        MockBalancingModel pool = new MockBalancingModel(
-//                containers(
-//                        containerA, 20, 50,
-//                        containerB, 20, 50),
-//                items(
-//                        "item1", containerA, 0,
-//                        "item2", containerB, -40,
-//                        "item3", containerB, 20,
-//                        "item4", containerB, 20)
-//        );
-//        
-//        BalancingStrategy<String, String> policy = new BalancingStrategy<String, String>("Test", pool);
-//        policy.rebalance();
-//        
-//        assertEquals((Object)pool.getItemsForContainer(containerA), ImmutableSet.of("item1", "item3", "item4"), pool.itemDistributionToString());
-//        assertEquals((Object)pool.getItemsForContainer(containerB), ImmutableSet.of("item2"), pool.itemDistributionToString());
-//    }
-
-    @Test
-    public void testMultiMoveBalancing() {
-        MockContainerEntity containerA = newContainer(app, "A", 20, 50);
-        MockContainerEntity containerB = newContainer(app, "B", 20, 50);
-        MockItemEntity item1 = newItem(app, containerA, "1", 10);
-        MockItemEntity item2 = newItem(app, containerA, "2", 10);
-        MockItemEntity item3 = newItem(app, containerA, "3", 10);
-        MockItemEntity item4 = newItem(app, containerA, "4", 10);
-        MockItemEntity item5 = newItem(app, containerA, "5", 10);
-        MockItemEntity item6 = newItem(app, containerA, "6", 10);
-        MockItemEntity item7 = newItem(app, containerA, "7", 10);
-        MockItemEntity item8 = newItem(app, containerA, "8", 10);
-        MockItemEntity item9 = newItem(app, containerA, "9", 10);
-        MockItemEntity item10 = newItem(app, containerA, "10", 10);
-
-        // non-deterministic which items will be moved; but can assert how many (given they all have same workrate)
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3, item4, item5, item6, item7, item8, item9, item10), 
-                ImmutableList.of(50d, 50d));
-        assertEquals(containerA.getBalanceableItems().size(), 5);
-        assertEquals(containerB.getBalanceableItems().size(), 5);
-    }
-    
-    @Test
-    public void testRebalanceWhenWorkratesChange() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
-        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
-        MockItemEntity item1 = newItem(app, containerA, "1", 0);
-        MockItemEntity item2 = newItem(app, containerA, "2", 0);
-
-        ((EntityLocal)item1).setAttribute(MockItemEntity.TEST_METRIC, 40);
-        ((EntityLocal)item2).setAttribute(MockItemEntity.TEST_METRIC, 40);
-        
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2), 
-                ImmutableList.of(40d, 40d));
-    }
-    
-    // Expect no balancing to occur in hot pool (2 containers over-threshold at 40).
-    // On addition of new container, expect hot containers to offload 10 each.
-    @Test
-    public void testAddContainerWhenHot() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 30);
-        MockContainerEntity containerB = newContainer(app, "B", 10, 30);
-        MockItemEntity item1 = newItem(app, containerA, "1", 10);
-        MockItemEntity item2 = newItem(app, containerA, "2", 10);
-        MockItemEntity item3 = newItem(app, containerA, "3", 10);
-        MockItemEntity item4 = newItem(app, containerA, "4", 10);
-        MockItemEntity item5 = newItem(app, containerB, "5", 10);
-        MockItemEntity item6 = newItem(app, containerB, "6", 10);
-        MockItemEntity item7 = newItem(app, containerB, "7", 10);
-        MockItemEntity item8 = newItem(app, containerB, "8", 10);
-        // Both containers are over-threshold at this point; should not rebalance.
-        
-        MockContainerEntity containerC = newAsyncContainer(app, "C", 10, 30, CONTAINER_STARTUP_DELAY_MS);
-        // New container allows hot ones to offload work.
-
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB, containerC), 
-                ImmutableList.of(item1, item2, item3, item4, item5, item6, item7, item8), 
-                ImmutableList.of(30d, 30d, 20d));
-    }
-
-    // On addition of new container, expect no rebalancing to occur as no existing container is hot.
-    @Test
-    public void testAddContainerWhenCold() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
-        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
-        MockItemEntity item1 = newItem(app, containerA, "1", 10);
-        MockItemEntity item2 = newItem(app, containerA, "2", 10);
-        MockItemEntity item3 = newItem(app, containerA, "3", 10);
-        MockItemEntity item4 = newItem(app, containerA, "4", 10);
-        MockItemEntity item5 = newItem(app, containerB, "5", 10);
-        MockItemEntity item6 = newItem(app, containerB, "6", 10);
-        MockItemEntity item7 = newItem(app, containerB, "7", 10);
-        MockItemEntity item8 = newItem(app, containerB, "8", 10);
-        
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3, item4, item5, item6, item7, item8), 
-                ImmutableList.of(40d, 40d));
-        
-        MockContainerEntity containerC = newAsyncContainer(app, "C", 10, 50, CONTAINER_STARTUP_DELAY_MS);
-
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB, containerC), 
-                ImmutableList.of(item1, item2, item3, item4, item5, item6, item7, item8), 
-                ImmutableList.of(40d, 40d, 0d));
-    }
-    
-    // Expect no balancing to occur in cool pool (2 containers under-threshold at 30).
-    // On addition of new item, expect over-threshold container (A) to offload 20 to B.
-    @Test
-    public void testAddItem() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
-        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
-        MockItemEntity item1 = newItem(app, containerA, "1", 10);
-        MockItemEntity item2 = newItem(app, containerA, "2", 10);
-        MockItemEntity item3 = newItem(app, containerA, "3", 10);
-        MockItemEntity item4 = newItem(app, containerB, "4", 10);
-        MockItemEntity item5 = newItem(app, containerB, "5", 10);
-        MockItemEntity item6 = newItem(app, containerB, "6", 10);
-        
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3, item4, item5, item6), 
-                ImmutableList.of(30d, 30d));
-        
-        newItem(app, containerA, "7", 40);
-        
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3, item4, item5, item6), 
-                ImmutableList.of(50d, 50d));
-    }
-    
-    // FIXME Failed in build repeatedly (e.g. #1035), but couldn't reproduce locally yet with invocationCount=100
-    @Test(groups="WIP")
-    public void testRemoveContainerCausesRebalancing() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 30);
-        MockContainerEntity containerB = newContainer(app, "B", 10, 30);
-        MockContainerEntity containerC = newContainer(app, "C", 10, 30);
-        MockItemEntity item1 = newItem(app, containerA, "1", 10);
-        MockItemEntity item2 = newItem(app, containerA, "2", 10);
-        MockItemEntity item3 = newItem(app, containerB, "3", 10);
-        MockItemEntity item4 = newItem(app, containerB, "4", 10);
-        MockItemEntity item5 = newItem(app, containerC, "5", 10);
-        MockItemEntity item6 = newItem(app, containerC, "6", 10);
-
-        Entities.unmanage(containerC);
-        item5.move(containerA);
-        item6.move(containerA);
-        
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3, item4, item5, item6), 
-                ImmutableList.of(30d, 30d));
-    }
-
-    @Test
-    public void testRemoveItemCausesRebalancing() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 30);
-        MockContainerEntity containerB = newContainer(app, "B", 10, 30);
-        MockItemEntity item1 = newItem(app, containerA, "1", 30);
-        MockItemEntity item2 = newItem(app, containerB, "2", 20);
-        MockItemEntity item3 = newItem(app, containerB, "3", 20);
-        
-        item1.stop();
-        Entities.unmanage(item1);
-        
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3), 
-                ImmutableList.of(20d, 20d));
-    }
-
-    @Test
-    public void testRebalancesAfterManualMove() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
-        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
-        MockItemEntity item1 = newItem(app, containerA, "1", 20);
-        MockItemEntity item2 = newItem(app, containerA, "2", 20);
-        MockItemEntity item3 = newItem(app, containerB, "3", 20);
-        MockItemEntity item4 = newItem(app, containerB, "4", 20);
-
-        // Move everything onto containerA, and expect it to be automatically re-balanced
-        item3.move(containerA);
-        item4.move(containerA);
-
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3, item4), 
-                ImmutableList.of(40d, 40d));
-    }
-    
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    @Test
-    public void testModelIncludesItemsAndContainersStartedBeforePolicyCreated() {
-        pool.removePolicy(policy);
-        policy.destroy();
-        
-        // Set-up containers and items.
-        final MockContainerEntity containerA = newContainer(app, "A", 10, 100);
-        newItem(app, containerA, "1", 10);
-
-        policy = new LoadBalancingPolicy(MutableMap.of(), TEST_METRIC, model);
-        pool.addPolicy(policy);
-        
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            public void run() {
-                assertEquals(model.getContainerWorkrates(), ImmutableMap.of(containerA, 10d));
-            }
-        });
-    }
-    
-    @Test
-    public void testLockedItemsNotMoved() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
-        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
-        MockItemEntity item1 = newLockedItem(app, containerA, "1", 40);
-        MockItemEntity item2 = newLockedItem(app, containerA, "2", 40);
-
-        assertWorkratesContinually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2), 
-                ImmutableList.of(80d, 0d));
-    }
-    
-    @Test
-    public void testLockedItemsContributeToOverloadedMeasurements() {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
-        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
-        MockItemEntity item1 = newLockedItem(app, containerA, "1", 40);
-        MockItemEntity item2 = newItem(app, containerA, "2", 25);
-        MockItemEntity item3 = newItem(app, containerA, "3", 25);
-        
-        assertWorkratesEventually(
-                ImmutableList.of(containerA, containerB),
-                ImmutableList.of(item1, item2, item3), 
-                ImmutableList.of(40d, 50d));
-    }
-    
-    @Test
-    public void testOverloadedLockedItemsPreventMoreWorkEnteringContainer() throws Exception {
-        // Set-up containers and items.
-        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
-        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
-        MockItemEntity item1 = newLockedItem(app, containerA, "1", 50);
-        Thread.sleep(1); // increase chances of item1's workrate having been received first
-        MockItemEntity item2 = newItem(app, containerB, "2", 30);
-        MockItemEntity item3 = newItem(app, containerB, "3", 30);
-        
-        assertWorkratesContinually(
-                ImmutableList.of(containerA, containerB), 
-                ImmutableList.of(item1, item2, item3), 
-                ImmutableList.of(50d, 60d));
-    }
-    
-    @Test
-    public void testPolicyUpdatesModel() {
-        final MockContainerEntity containerA = newContainer(app, "A", 10, 20);
-        final MockContainerEntity containerB = newContainer(app, "B", 11, 21);
-        final MockItemEntity item1 = newItem(app, containerA, "1", 12);
-        final MockItemEntity item2 = newItem(app, containerB, "2", 13);
-        
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            public void run() {
-                assertEquals(model.getPoolSize(), 2);
-                assertEquals(model.getPoolContents(), ImmutableSet.of(containerA, containerB));
-                assertEquals(model.getItemWorkrate(item1), 12d);
-                assertEquals(model.getItemWorkrate(item2), 13d);
-                
-                assertEquals(model.getParentContainer(item1), containerA);
-                assertEquals(model.getParentContainer(item2), containerB);
-                assertEquals(model.getContainerWorkrates(), ImmutableMap.of(containerA, 12d, containerB, 13d));
-                
-                assertEquals(model.getPoolLowThreshold(), 10+11d);
-                assertEquals(model.getPoolHighThreshold(), 20+21d);
-                assertEquals(model.getCurrentPoolWorkrate(), 12+13d);
-                assertFalse(model.isHot());
-                assertFalse(model.isCold());
-            }
-        });
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/MockContainerEntity.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/MockContainerEntity.java b/policy/src/test/java/brooklyn/policy/loadbalancing/MockContainerEntity.java
deleted file mode 100644
index 70d4b57..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/MockContainerEntity.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import java.util.Map;
-
-import org.apache.brooklyn.api.entity.Effector;
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.annotation.EffectorParam;
-import brooklyn.entity.basic.AbstractGroup;
-import brooklyn.entity.basic.MethodEffector;
-import brooklyn.entity.trait.Startable;
-import brooklyn.event.basic.BasicConfigKey;
-
-@ImplementedBy(MockContainerEntityImpl.class)
-public interface MockContainerEntity extends AbstractGroup, BalanceableContainer<Movable>, Startable {
-
-    @SetFromFlag("membership")
-    public static final ConfigKey<String> MOCK_MEMBERSHIP = new BasicConfigKey<String>(
-            String.class, "mock.container.membership", "For testing ItemsInContainersGroup");
-
-    @SetFromFlag("delay")
-    public static final ConfigKey<Long> DELAY = new BasicConfigKey<Long>(
-            Long.class, "mock.container.delay", "", 0L);
-
-    public static final Effector<Void> OFFLOAD_AND_STOP = new MethodEffector<Void>(MockContainerEntity.class, "offloadAndStop");
-
-    public void lock();
-
-    public void unlock();
-
-    public int getWorkrate();
-
-    public Map<Entity, Double> getItemUsage();
-
-    public void addItem(Entity item);
-
-    public void removeItem(Entity item);
-
-    public void offloadAndStop(@EffectorParam(name="otherContianer") MockContainerEntity otherContainer);
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/MockContainerEntityImpl.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/MockContainerEntityImpl.java b/policy/src/test/java/brooklyn/policy/loadbalancing/MockContainerEntityImpl.java
deleted file mode 100644
index 672addb..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/MockContainerEntityImpl.java
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.locks.ReentrantLock;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.event.AttributeSensor;
-import org.apache.brooklyn.api.location.Location;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.entity.basic.AbstractGroupImpl;
-import brooklyn.entity.basic.Attributes;
-import brooklyn.util.collections.MutableList;
-import brooklyn.util.time.Time;
-
-import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-
-
-public class MockContainerEntityImpl extends AbstractGroupImpl implements MockContainerEntity {
-
-    private static final Logger LOG = LoggerFactory.getLogger(MockContainerEntity.class);
-
-    volatile boolean offloading;
-    volatile boolean running;
-
-    ReentrantLock _lock = new ReentrantLock();
-
-    @Override
-    public <T> T setAttribute(AttributeSensor<T> attribute, T val) {
-        if (LOG.isDebugEnabled()) LOG.debug("Mocks: container {} setting {} to {}", new Object[] {this, attribute, val});
-        return super.setAttribute(attribute, val);
-    }
-
-    @Override
-    public void lock() {
-        _lock.lock();
-        if (!running) {
-            _lock.unlock();
-            throw new IllegalStateException("Container lock "+this+"; it is not running");
-        }
-    }
-
-    @Override
-    public void unlock() {
-        _lock.unlock();
-    }
-
-    @Override
-    public int getWorkrate() {
-        int result = 0;
-        for (Entity member : getMembers()) {
-            Integer memberMetric = member.getAttribute(MockItemEntity.TEST_METRIC);
-            result += ((memberMetric != null) ? memberMetric : 0);
-        }
-        return result;
-    }
-
-    @Override
-    public Map<Entity, Double> getItemUsage() {
-        Map<Entity, Double> result = Maps.newLinkedHashMap();
-        for (Entity member : getMembers()) {
-            Map<Entity, Double> memberItemUsage = member.getAttribute(MockItemEntity.ITEM_USAGE_METRIC);
-            if (memberItemUsage != null) {
-                for (Map.Entry<Entity, Double> entry : memberItemUsage.entrySet()) {
-                    double val = (result.containsKey(entry.getKey()) ? result.get(entry.getKey()) : 0d);
-                    val += ((entry.getValue() != null) ? entry.getValue() : 0);
-                    result.put(entry.getKey(), val);
-                }
-            }
-        }
-        return result;
-    }
-    
-    @Override
-    public void addItem(Entity item) {
-        if (LOG.isDebugEnabled()) LOG.debug("Mocks: adding item {} to container {}", item, this);
-        if (!running || offloading) throw new IllegalStateException("Container "+getDisplayName()+" is not running; cannot add item "+item);
-        addMember(item);
-        emit(BalanceableContainer.ITEM_ADDED, item);
-    }
-
-    @Override
-    public void removeItem(Entity item) {
-        if (LOG.isDebugEnabled()) LOG.debug("Mocks: removing item {} from container {}", item, this);
-        if (!running) throw new IllegalStateException("Container "+getDisplayName()+" is not running; cannot remove item "+item);
-        removeMember(item);
-        emit(BalanceableContainer.ITEM_REMOVED, item);
-    }
-
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    @Override
-    public Set<Movable> getBalanceableItems() {
-        return (Set) Sets.newLinkedHashSet(getMembers());
-    }
-
-    public String toString() {
-        return "MockContainer["+getDisplayName()+"]";
-    }
-
-    private long getDelay() {
-        return getConfig(DELAY);
-    }
-    
-    @Override
-    public void start(Collection<? extends Location> locs) {
-        if (LOG.isDebugEnabled()) LOG.debug("Mocks: starting container {}", this);
-        _lock.lock();
-        try {
-            Time.sleep(getDelay());
-            running = true;
-            addLocations(locs);
-            emit(Attributes.LOCATION_CHANGED, null);
-            setAttribute(SERVICE_UP, true);
-        } finally {
-            _lock.unlock();
-        }
-    }
-
-    @Override
-    public void stop() {
-        if (LOG.isDebugEnabled()) LOG.debug("Mocks: stopping container {}", this);
-        _lock.lock();
-        try {
-            running = false;
-            Time.sleep(getDelay());
-            setAttribute(SERVICE_UP, false);
-        } finally {
-            _lock.unlock();
-        }
-    }
-
-    private void stopWithoutLock() {
-        running = false;
-        Time.sleep(getDelay());
-        setAttribute(SERVICE_UP, false);
-    }
-
-    public void offloadAndStop(final MockContainerEntity otherContainer) {
-        if (LOG.isDebugEnabled()) LOG.debug("Mocks: offloading container {} to {} (items {})", new Object[] {this, otherContainer, getBalanceableItems()});
-        runWithLock(ImmutableList.of(this, otherContainer), new Runnable() {
-            public void run() {
-                offloading = false;
-                for (Movable item : getBalanceableItems()) {
-                    ((MockItemEntity)item).moveNonEffector(otherContainer);
-                }
-                if (LOG.isDebugEnabled()) LOG.debug("Mocks: stopping offloaded container {}", this);
-                stopWithoutLock();
-            }});
-    }
-
-    @Override
-    public void restart() {
-        if (LOG.isDebugEnabled()) LOG.debug("Mocks: restarting {}", this);
-        throw new UnsupportedOperationException();
-    }
-
-    public static void runWithLock(List<MockContainerEntity> entitiesToLock, Runnable r) {
-        List<MockContainerEntity> entitiesToLockCopy = MutableList.copyOf(Iterables.filter(entitiesToLock, Predicates.notNull()));
-        List<MockContainerEntity> entitiesLocked = Lists.newArrayList();
-        Collections.sort(entitiesToLockCopy, new Comparator<MockContainerEntity>() {
-            public int compare(MockContainerEntity o1, MockContainerEntity o2) {
-                return o1.getId().compareTo(o2.getId());
-            }});
-
-        try {
-            for (MockContainerEntity it : entitiesToLockCopy) {
-                it.lock();
-                entitiesLocked.add(it);
-            }
-
-            r.run();
-
-        } finally {
-            for (MockContainerEntity it : entitiesLocked) {
-                it.unlock();
-            }
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/MockItemEntity.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/MockItemEntity.java b/policy/src/test/java/brooklyn/policy/loadbalancing/MockItemEntity.java
deleted file mode 100644
index 9286072..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/MockItemEntity.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import java.util.Map;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
-import org.apache.brooklyn.api.event.AttributeSensor;
-
-import brooklyn.event.basic.Sensors;
-
-import com.google.common.reflect.TypeToken;
-
-@ImplementedBy(MockItemEntityImpl.class)
-public interface MockItemEntity extends Entity, Movable {
-
-    public static final AttributeSensor<Integer> TEST_METRIC = Sensors.newIntegerSensor(
-            "test.metric", "Dummy workrate for test entities");
-
-    @SuppressWarnings("serial")
-    public static final AttributeSensor<Map<Entity, Double>> ITEM_USAGE_METRIC = Sensors.newSensor(
-            new TypeToken<Map<Entity, Double>>() {}, "test.itemUsage.metric", "Dummy item usage for test entities");
-
-    public boolean isStopped();
-
-    public void moveNonEffector(Entity rawDestination);
-    
-    public void stop();
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/MockItemEntityImpl.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/MockItemEntityImpl.java b/policy/src/test/java/brooklyn/policy/loadbalancing/MockItemEntityImpl.java
deleted file mode 100644
index ecc725e..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/MockItemEntityImpl.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.locks.ReentrantLock;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.event.AttributeSensor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.entity.basic.AbstractEntity;
-import brooklyn.util.collections.MutableList;
-
-
-public class MockItemEntityImpl extends AbstractEntity implements MockItemEntity {
-
-    private static final Logger LOG = LoggerFactory.getLogger(MockItemEntityImpl.class);
-    
-    public static AtomicInteger totalMoveCount = new AtomicInteger(0);
-    
-    public static AtomicLong lastMoveTime = new AtomicLong(-1);
-    
-    private volatile boolean stopped;
-    private volatile MockContainerEntity currentContainer;
-    
-    private final ReentrantLock _lock = new ReentrantLock();
-    
-    @Override
-    public String getContainerId() {
-        return (currentContainer == null) ? null : currentContainer.getId();
-    }
-
-    @Override
-    public boolean isStopped() {
-        return stopped;
-    }
-
-    @Override
-    public <T> T setAttribute(AttributeSensor<T> attribute, T val) {
-        if (LOG.isDebugEnabled()) LOG.debug("Mocks: item {} setting {} to {}", new Object[] {this, attribute, val});
-        return super.setAttribute(attribute, val);
-    }
-
-    @Override
-    public void move(Entity destination) {
-        totalMoveCount.incrementAndGet();
-        lastMoveTime.set(System.currentTimeMillis());
-        moveNonEffector(destination);
-    }
-    
-    // only moves if the containers will accept us (otherwise we'd lose the item!)
-    @Override
-    public void moveNonEffector(Entity rawDestination) {
-        if (LOG.isDebugEnabled()) LOG.debug("Mocks: moving item {} from {} to {}", new Object[] {this, currentContainer, rawDestination});
-        checkNotNull(rawDestination);
-        final MockContainerEntity previousContainer = currentContainer;
-        final MockContainerEntity destination = (MockContainerEntity) rawDestination;
-        
-        MockContainerEntityImpl.runWithLock(MutableList.of(previousContainer, destination), new Runnable() { 
-            @Override public void run() {
-                _lock.lock();
-                try {
-                    if (stopped) throw new IllegalStateException("Item "+this+" is stopped; cannot move to "+destination);
-                    if (currentContainer != null) currentContainer.removeItem(MockItemEntityImpl.this);
-                    currentContainer = destination;
-                    destination.addItem(MockItemEntityImpl.this);
-                    setAttribute(CONTAINER, currentContainer);
-                } finally {
-                    _lock.unlock();
-                }
-        }});
-    }
-    
-    @Override
-    public void stop() {
-        // FIXME How best to indicate this has been entirely stopped, rather than just in-transit?
-        if (LOG.isDebugEnabled()) LOG.debug("Mocks: stopping item {} (was in container {})", this, currentContainer);
-        _lock.lock();
-        try {
-            if (currentContainer != null) currentContainer.removeItem(this);
-            currentContainer = null;
-            stopped = true;
-        } finally {
-            _lock.unlock();
-        }
-    }
-    
-    @Override
-    public String toString() {
-        return "MockItem["+getDisplayName()+"]";
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyMetricTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyMetricTest.java b/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyMetricTest.java
new file mode 100644
index 0000000..83f7b33
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyMetricTest.java
@@ -0,0 +1,274 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.autoscaling;
+
+import static org.apache.brooklyn.policy.autoscaling.AutoScalerPolicyTest.currentSizeAsserter;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.event.AttributeSensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestCluster;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.event.basic.BasicNotificationSensor;
+import brooklyn.event.basic.Sensors;
+import brooklyn.test.Asserts;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+public class AutoScalerPolicyMetricTest {
+    
+    private static long TIMEOUT_MS = 10000;
+    private static long SHORT_WAIT_MS = 50;
+    
+    private static final AttributeSensor<Integer> MY_ATTRIBUTE = Sensors.newIntegerSensor("autoscaler.test.intAttrib");
+    TestApplication app;
+    TestCluster tc;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void before() {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        tc = app.createAndManageChild(EntitySpec.create(TestCluster.class)
+                .configure("initialSize", 1));
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+
+    @Test
+    public void testIncrementsSizeIffUpperBoundExceeded() {
+        tc.resize(1);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
+        tc.addPolicy(policy);
+
+        tc.setAttribute(MY_ATTRIBUTE, 100);
+        Asserts.succeedsContinually(ImmutableMap.of("timeout", SHORT_WAIT_MS), currentSizeAsserter(tc, 1));
+
+        tc.setAttribute(MY_ATTRIBUTE, 101);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
+    }
+    
+    @Test
+    public void testDecrementsSizeIffLowerBoundExceeded() {
+        tc.resize(2);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
+        tc.addPolicy(policy);
+
+        tc.setAttribute(MY_ATTRIBUTE, 50);
+        Asserts.succeedsContinually(ImmutableMap.of("timeout", SHORT_WAIT_MS), currentSizeAsserter(tc, 2));
+
+        tc.setAttribute(MY_ATTRIBUTE, 49);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 1));
+    }
+    
+    @Test(groups="Integration")
+    public void testIncrementsSizeInProportionToMetric() {
+        tc.resize(5);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
+        tc.addPolicy(policy);
+        
+        // workload 200 so requires doubling size to 10 to handle: (200*5)/100 = 10
+        tc.setAttribute(MY_ATTRIBUTE, 200);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 10));
+        
+        // workload 5, requires 1 entity: (10*110)/100 = 11
+        tc.setAttribute(MY_ATTRIBUTE, 110);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 11));
+    }
+    
+    @Test(groups="Integration")
+    public void testDecrementsSizeInProportionToMetric() {
+        tc.resize(5);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
+        tc.addPolicy(policy);
+        
+        // workload can be handled by 4 servers, within its valid range: (49*5)/50 = 4.9
+        tc.setAttribute(MY_ATTRIBUTE, 49);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 4));
+        
+        // workload can be handled by 4 servers, within its valid range: (25*4)/50 = 2
+        tc.setAttribute(MY_ATTRIBUTE, 25);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
+        
+        tc.setAttribute(MY_ATTRIBUTE, 0);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 1));
+    }
+    
+    @Test(groups="Integration")
+    public void testObeysMinAndMaxSize() {
+        tc.resize(4);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
+                .metricLowerBound(50).metricUpperBound(100)
+                .minPoolSize(2).maxPoolSize(6)
+                .build();
+        tc.addPolicy(policy);
+
+        // Decreases to min-size only
+        tc.setAttribute(MY_ATTRIBUTE, 0);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
+        
+        // Increases to max-size only
+        tc.setAttribute(MY_ATTRIBUTE, 100000);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 6));
+    }
+    
+    @Test(groups="Integration",invocationCount=20)
+    public void testWarnsWhenMaxCapReached() {
+        final List<MaxPoolSizeReachedEvent> maxReachedEvents = Lists.newCopyOnWriteArrayList();
+        tc.resize(1);
+        
+        BasicNotificationSensor<MaxPoolSizeReachedEvent> maxSizeReachedSensor = AutoScalerPolicy.DEFAULT_MAX_SIZE_REACHED_SENSOR;
+        
+        app.subscribe(tc, maxSizeReachedSensor, new SensorEventListener<MaxPoolSizeReachedEvent>() {
+                @Override public void onEvent(SensorEvent<MaxPoolSizeReachedEvent> event) {
+                    maxReachedEvents.add(event.getValue());
+                }});
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
+                .metricLowerBound(50).metricUpperBound(100)
+                .maxPoolSize(6)
+                .maxSizeReachedSensor(maxSizeReachedSensor)
+                .build();
+        tc.addPolicy(policy);
+
+        // workload can be handled by 6 servers, so no need to notify: 6 <= (100*6)/50
+        tc.setAttribute(MY_ATTRIBUTE, 600);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 6));
+        assertTrue(maxReachedEvents.isEmpty());
+        
+        // Increases to above max capacity: would require (100000*6)/100 = 6000
+        tc.setAttribute(MY_ATTRIBUTE, 100000);
+        
+        // Assert our listener gets notified (once)
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            public void run() {
+                assertEquals(maxReachedEvents.size(), 1);
+                assertEquals(maxReachedEvents.get(0).getMaxAllowed(), 6);
+                assertEquals(maxReachedEvents.get(0).getCurrentPoolSize(), 6);
+                assertEquals(maxReachedEvents.get(0).getCurrentUnbounded(), 6000);
+                assertEquals(maxReachedEvents.get(0).getMaxUnbounded(), 6000);
+                assertEquals(maxReachedEvents.get(0).getTimeWindow(), 0);
+            }});
+        Asserts.succeedsContinually(new Runnable() {
+                @Override public void run() {
+                    assertEquals(maxReachedEvents.size(), 1);
+                }});
+        currentSizeAsserter(tc, 6).run();
+    }
+    
+    @Test
+    public void testDestructionState() {
+        tc.resize(1);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
+        tc.addPolicy(policy);
+
+        policy.destroy();
+        assertTrue(policy.isDestroyed());
+        assertFalse(policy.isRunning());
+        
+        tc.setAttribute(MY_ATTRIBUTE, 100000);
+        Asserts.succeedsContinually(ImmutableMap.of("timeout", SHORT_WAIT_MS), currentSizeAsserter(tc, 1));
+        
+        // TODO Could assert all subscriptions have been de-registered as well, 
+        // but that requires exposing more things just for testing...
+    }
+    
+    @Test
+    public void testSuspendState() {
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
+        tc.addPolicy(policy);
+        
+        policy.suspend();
+        assertFalse(policy.isRunning());
+        assertFalse(policy.isDestroyed());
+        
+        policy.resume();
+        assertTrue(policy.isRunning());
+        assertFalse(policy.isDestroyed());
+    }
+
+    @Test
+    public void testPostSuspendActions() {
+        tc.resize(1);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
+        tc.addPolicy(policy);
+
+        policy.suspend();
+        
+        tc.setAttribute(MY_ATTRIBUTE, 100000);
+        Asserts.succeedsContinually(ImmutableMap.of("timeout", SHORT_WAIT_MS), currentSizeAsserter(tc, 1));
+    }
+    
+    @Test
+    public void testPostResumeActions() {
+        tc.resize(1);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
+        tc.addPolicy(policy);
+        
+        policy.suspend();
+        policy.resume();
+        tc.setAttribute(MY_ATTRIBUTE, 101);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
+    }
+    
+    @Test
+    public void testSubscribesToMetricOnSpecifiedEntity() {
+        TestEntity entityWithMetric = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        
+        tc.resize(1);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder()
+                .metric(TestEntity.SEQUENCE)
+                .entityWithMetric(entityWithMetric)
+                .metricLowerBound(50)
+                .metricUpperBound(100)
+                .build();
+        tc.addPolicy(policy);
+
+        // First confirm that tc is not being listened to for this entity
+        tc.setAttribute(TestEntity.SEQUENCE, 101);
+        Asserts.succeedsContinually(ImmutableMap.of("timeout", SHORT_WAIT_MS), currentSizeAsserter(tc, 1));
+
+        // Then confirm we listen to the correct "entityWithMetric"
+        entityWithMetric.setAttribute(TestEntity.SEQUENCE, 101);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyRebindTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyRebindTest.java b/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyRebindTest.java
new file mode 100644
index 0000000..f8f200b
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyRebindTest.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.autoscaling;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.event.AttributeSensor;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.rebind.RebindTestFixtureWithApp;
+import brooklyn.event.basic.BasicNotificationSensor;
+import brooklyn.event.basic.Sensors;
+
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+
+import brooklyn.util.time.Duration;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class AutoScalerPolicyRebindTest extends RebindTestFixtureWithApp {
+
+    public static BasicNotificationSensor<Map> POOL_HOT_SENSOR = new BasicNotificationSensor<Map>(
+            Map.class, "AutoScalerPolicyRebindTest.resizablepool.hot", "Pool is over-utilized; it has insufficient resource for current workload");
+    public static BasicNotificationSensor<Map> POOL_COLD_SENSOR = new BasicNotificationSensor<Map>(
+            Map.class, "AutoScalerPolicyRebindTest.resizablepool.cold", "Pool is under-utilized; it has too much resource for current workload");
+    public static BasicNotificationSensor<Map> POOL_OK_SENSOR = new BasicNotificationSensor<Map>(
+            Map.class, "AutoScalerPolicyRebindTest.resizablepool.cold", "Pool utilization is ok; the available resources are fine for the current workload");
+    public static BasicNotificationSensor<MaxPoolSizeReachedEvent> MAX_SIZE_REACHED_SENSOR = new BasicNotificationSensor<MaxPoolSizeReachedEvent>(
+            MaxPoolSizeReachedEvent.class, "AutoScalerPolicyRebindTest.maxSizeReached");
+    public static AttributeSensor<Integer> METRIC_SENSOR = Sensors.newIntegerSensor("AutoScalerPolicyRebindTest.metric");
+            
+    private DynamicCluster origCluster;
+    private SimulatedLocation origLoc;
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        origLoc = origManagementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
+        origCluster = origApp.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("memberSpec", EntitySpec.create(TestEntity.class)));
+    }
+    
+    @Test
+    public void testRestoresAutoScalerConfig() throws Exception {
+        origCluster.addPolicy(AutoScalerPolicy.builder()
+                .name("myname")
+                .metric(METRIC_SENSOR)
+                .entityWithMetric(origCluster)
+                .metricUpperBound(1)
+                .metricLowerBound(2)
+                .minPoolSize(0)
+                .maxPoolSize(3)
+                .minPeriodBetweenExecs(Duration.of(4, TimeUnit.MILLISECONDS))
+                .resizeUpStabilizationDelay(Duration.of(5, TimeUnit.MILLISECONDS))
+                .resizeDownStabilizationDelay(Duration.of(6, TimeUnit.MILLISECONDS))
+                .poolHotSensor(POOL_HOT_SENSOR)
+                .poolColdSensor(POOL_COLD_SENSOR)
+                .poolOkSensor(POOL_OK_SENSOR)
+                .maxSizeReachedSensor(MAX_SIZE_REACHED_SENSOR)
+                .maxReachedNotificationDelay(Duration.of(7, TimeUnit.MILLISECONDS))
+                .buildSpec());
+        
+        TestApplication newApp = rebind();
+        DynamicCluster newCluster = (DynamicCluster) Iterables.getOnlyElement(newApp.getChildren());
+        AutoScalerPolicy newPolicy = (AutoScalerPolicy) Iterables.getOnlyElement(newCluster.getPolicies());
+
+        assertEquals(newPolicy.getDisplayName(), "myname");
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.METRIC), METRIC_SENSOR);
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.ENTITY_WITH_METRIC), newCluster);
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.METRIC_UPPER_BOUND), 1);
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.METRIC_LOWER_BOUND), 2);
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.MIN_POOL_SIZE), (Integer)0);
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.MAX_POOL_SIZE), (Integer)3);
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.MIN_PERIOD_BETWEEN_EXECS), Duration.of(4, TimeUnit.MILLISECONDS));
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.RESIZE_UP_STABILIZATION_DELAY), Duration.of(5, TimeUnit.MILLISECONDS));
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.RESIZE_DOWN_STABILIZATION_DELAY), Duration.of(6, TimeUnit.MILLISECONDS));
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.POOL_HOT_SENSOR), POOL_HOT_SENSOR);
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.POOL_COLD_SENSOR), POOL_COLD_SENSOR);
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.POOL_OK_SENSOR), POOL_OK_SENSOR);
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.MAX_SIZE_REACHED_SENSOR), MAX_SIZE_REACHED_SENSOR);
+        assertEquals(newPolicy.getConfig(AutoScalerPolicy.MAX_REACHED_NOTIFICATION_DELAY), Duration.of(7, TimeUnit.MILLISECONDS));
+    }
+    
+    @Test
+    public void testAutoScalerResizesAfterRebind() throws Exception {
+        origCluster.start(ImmutableList.of(origLoc));
+        origCluster.addPolicy(AutoScalerPolicy.builder()
+                .name("myname")
+                .metric(METRIC_SENSOR)
+                .entityWithMetric(origCluster)
+                .metricUpperBound(10)
+                .metricLowerBound(100)
+                .minPoolSize(1)
+                .maxPoolSize(3)
+                .buildSpec());
+        
+        TestApplication newApp = rebind();
+        DynamicCluster newCluster = (DynamicCluster) Iterables.getOnlyElement(newApp.getChildren());
+
+        assertEquals(newCluster.getCurrentSize(), (Integer)1);
+        
+        ((EntityInternal)newCluster).setAttribute(METRIC_SENSOR, 1000);
+        EntityTestUtils.assertGroupSizeEqualsEventually(newCluster, 3);
+        
+        ((EntityInternal)newCluster).setAttribute(METRIC_SENSOR, 1);
+        EntityTestUtils.assertGroupSizeEqualsEventually(newCluster, 1);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyReconfigurationTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyReconfigurationTest.java b/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyReconfigurationTest.java
new file mode 100644
index 0000000..f91adee
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyReconfigurationTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.autoscaling;
+
+import static org.apache.brooklyn.policy.autoscaling.AutoScalerPolicyTest.currentSizeAsserter;
+
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.event.AttributeSensor;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestCluster;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.event.basic.Sensors;
+import brooklyn.test.Asserts;
+import brooklyn.util.time.Duration;
+
+import com.google.common.collect.ImmutableMap;
+
+public class AutoScalerPolicyReconfigurationTest {
+    
+    private static long TIMEOUT_MS = 10000;
+    
+    private static final AttributeSensor<Integer> MY_ATTRIBUTE = Sensors.newIntegerSensor("autoscaler.test.intAttrib");
+    TestApplication app;
+    TestCluster tc;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void before() throws Exception {
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        tc = app.createAndManageChild(EntitySpec.create(TestCluster.class)
+                .configure("initialSize", 1));
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+
+    @Test
+    public void testIncreaseMinPoolSizeCausesImmediateGrowth() {
+        tc.resize(2);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
+                .metricLowerBound(50).metricUpperBound(100)
+                .minPoolSize(2)
+                .build();
+        tc.addPolicy(policy);
+
+        policy.config().set(AutoScalerPolicy.MIN_POOL_SIZE, 3);
+        
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 3));
+    }
+    
+    @Test
+    public void testDecreaseMinPoolSizeAllowsSubsequentShrink() {
+        tc.resize(4);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
+                .metricLowerBound(50).metricUpperBound(100)
+                .minPoolSize(2)
+                .build();
+        tc.addPolicy(policy);
+        
+        // 25*4 = 100 -> 2 nodes at 50 each
+        tc.setAttribute(MY_ATTRIBUTE, 25);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
+
+        // Decreases to new min-size
+        policy.config().set(AutoScalerPolicy.MIN_POOL_SIZE, 1);
+        tc.setAttribute(MY_ATTRIBUTE, 0);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 1));
+    }
+    
+    @Test
+    public void testDecreaseMaxPoolSizeCausesImmediateShrink() {
+        tc.resize(6);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
+                .metricLowerBound(50).metricUpperBound(100)
+                .maxPoolSize(6)
+                .build();
+        tc.addPolicy(policy);
+
+        policy.config().set(AutoScalerPolicy.MAX_POOL_SIZE, 4);
+        
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 4));
+    }
+    
+    @Test
+    public void testIncreaseMaxPoolSizeAllowsSubsequentGrowth() {
+        tc.resize(3);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
+                .metricLowerBound(50).metricUpperBound(100)
+                .maxPoolSize(6)
+                .build();
+        tc.addPolicy(policy);
+
+        // 200*3 = 600 -> 6 nodes at 100 each
+        tc.setAttribute(MY_ATTRIBUTE, 200);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 6));
+        
+        policy.config().set(AutoScalerPolicy.MAX_POOL_SIZE, 8);
+        
+        // Increases to max-size only
+        tc.setAttribute(MY_ATTRIBUTE, 100000);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 8));
+    }
+    
+    @Test
+    public void testReconfigureMetricLowerBound() {
+        tc.resize(2);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
+                .metricLowerBound(50).metricUpperBound(100)
+                .build();
+        tc.addPolicy(policy);
+
+        policy.config().set(AutoScalerPolicy.METRIC_LOWER_BOUND, 51);
+
+        tc.setAttribute(MY_ATTRIBUTE, 50);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 1));
+    }
+
+    @Test
+    public void testReconfigureMetricUpperBound() {
+        tc.resize(1);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
+                .metricLowerBound(50).metricUpperBound(100)
+                .build();
+        tc.addPolicy(policy);
+
+        policy.config().set(AutoScalerPolicy.METRIC_UPPER_BOUND, 99);
+
+        tc.setAttribute(MY_ATTRIBUTE, 100);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
+    }
+
+    @Test
+    public void testReconfigureResizeUpStabilizationDelay() {
+        tc.resize(1);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
+                .metricLowerBound(50).metricUpperBound(100)
+                .resizeUpStabilizationDelay(Duration.TWO_MINUTES)
+                .build();
+        tc.addPolicy(policy);
+
+        policy.config().set(AutoScalerPolicy.RESIZE_UP_STABILIZATION_DELAY, Duration.ZERO);
+
+        tc.setAttribute(MY_ATTRIBUTE, 101);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
+    }
+    
+    @Test
+    public void testReconfigureResizeDownStabilizationDelay() {
+        tc.resize(2);
+        
+        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
+                .metricLowerBound(50).metricUpperBound(100)
+                .resizeDownStabilizationDelay(Duration.TWO_MINUTES)
+                .build();
+        tc.addPolicy(policy);
+
+        policy.config().set(AutoScalerPolicy.RESIZE_DOWN_STABILIZATION_DELAY, Duration.ZERO);
+
+        tc.setAttribute(MY_ATTRIBUTE, 1);
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 1));
+    }
+}


[03/24] incubator-brooklyn git commit: escape user input to sql

Posted by he...@apache.org.
escape user input to sql


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

Branch: refs/heads/master
Commit: ac82d23e49cdeb0ecbb3e77872bb26c0322c02d9
Parents: 9458e15
Author: Robert Moss <ro...@gmail.com>
Authored: Mon Aug 17 14:54:43 2015 +0100
Committer: Robert Moss <ro...@gmail.com>
Committed: Mon Aug 17 14:54:43 2015 +0100

----------------------------------------------------------------------
 .../entity/database/postgresql/PostgreSqlSshDriver.java      | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ac82d23e/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
----------------------------------------------------------------------
diff --git a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
index 18dc9a4..0242af1 100644
--- a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
+++ b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
@@ -62,6 +62,7 @@ import brooklyn.util.task.ssh.SshTasks;
 import brooklyn.util.task.ssh.SshTasks.OnFailingTask;
 import brooklyn.util.task.system.ProcessTaskWrapper;
 import brooklyn.util.text.Identifiers;
+import brooklyn.util.text.StringEscapes;
 import brooklyn.util.text.StringFunctions;
 import brooklyn.util.text.Strings;
 
@@ -297,12 +298,13 @@ public class PostgreSqlSshDriver extends AbstractSoftwareProcessSshDriver implem
         DynamicTasks.waitForLast();
         String createUserCommand = String.format(
                 "\"CREATE USER %s WITH PASSWORD '%s'; \"",
-                entity.getConfig(PostgreSqlNode.USERNAME), getUserPassword()
+                StringEscapes.escapeSql(entity.getConfig(PostgreSqlNode.USERNAME)), 
+                StringEscapes.escapeSql(getUserPassword())
         );
         String createDatabaseCommand = String.format(
                 "\"CREATE DATABASE %s OWNER %s\"",
-                entity.getConfig(PostgreSqlNode.DATABASE),
-                entity.getConfig(PostgreSqlNode.USERNAME));
+                StringEscapes.escapeSql(entity.getConfig(PostgreSqlNode.DATABASE)),
+                StringEscapes.escapeSql(entity.getConfig(PostgreSqlNode.USERNAME)));
         newScript("initializing user and database")
         .body.append(
                 "cd " + getInstallDir(),


[14/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceFailureDetector.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceFailureDetector.java b/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceFailureDetector.java
new file mode 100644
index 0000000..80feb4c
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceFailureDetector.java
@@ -0,0 +1,340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.core.util.config.ConfigBag;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+import org.apache.brooklyn.core.util.task.BasicTask;
+import org.apache.brooklyn.core.util.task.ScheduledTask;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
+import brooklyn.entity.basic.ServiceStateLogic.ComputeServiceState;
+import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.event.basic.BasicNotificationSensor;
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+
+/** 
+ * Emits {@link HASensors#ENTITY_FAILED} whenever the parent's default logic ({@link ComputeServiceState}) would detect a problem,
+ * and similarly {@link HASensors#ENTITY_RECOVERED} when recovered.
+ * <p>
+ * gives more control over suppressing {@link Lifecycle#ON_FIRE}, 
+ * for some period of time
+ * (or until another process manually sets {@link Attributes#SERVICE_STATE_ACTUAL} to {@value Lifecycle#ON_FIRE},
+ * which this enricher will not clear until all problems have gone away)
+ */
+//@Catalog(name="Service Failure Detector", description="HA policy for deteting failure of a service")
+public class ServiceFailureDetector extends ServiceStateLogic.ComputeServiceState {
+
+    // TODO Remove duplication between this and MemberFailureDetectionPolicy.
+    // The latter could be re-written to use this. Or could even be deprecated
+    // in favour of this.
+
+    public enum LastPublished {
+        NONE,
+        FAILED,
+        RECOVERED;
+    }
+
+    private static final Logger LOG = LoggerFactory.getLogger(ServiceFailureDetector.class);
+
+    private static final long MIN_PERIOD_BETWEEN_EXECS_MILLIS = 100;
+
+    public static final BasicNotificationSensor<FailureDescriptor> ENTITY_FAILED = HASensors.ENTITY_FAILED;
+
+    @SetFromFlag("onlyReportIfPreviouslyUp")
+    public static final ConfigKey<Boolean> ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP = ConfigKeys.newBooleanConfigKey("onlyReportIfPreviouslyUp", 
+        "Prevents the policy from emitting ENTITY_FAILED if the entity fails on startup (ie has never been up)", true);
+    
+    public static final ConfigKey<Boolean> MONITOR_SERVICE_PROBLEMS = ConfigKeys.newBooleanConfigKey("monitorServiceProblems", 
+        "Whether to monitor service problems, and emit on failures there (if set to false, this monitors only service up)", true);
+
+    @SetFromFlag("serviceOnFireStabilizationDelay")
+    public static final ConfigKey<Duration> SERVICE_ON_FIRE_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
+            .name("serviceOnFire.stabilizationDelay")
+            .description("Time period for which the service must be consistently down for (e.g. doesn't report down-up-down) before concluding ON_FIRE")
+            .defaultValue(Duration.ZERO)
+            .build();
+
+    @SetFromFlag("entityFailedStabilizationDelay")
+    public static final ConfigKey<Duration> ENTITY_FAILED_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
+            .name("entityFailed.stabilizationDelay")
+            .description("Time period for which the service must be consistently down for (e.g. doesn't report down-up-down) before emitting ENTITY_FAILED")
+            .defaultValue(Duration.ZERO)
+            .build();
+
+    @SetFromFlag("entityRecoveredStabilizationDelay")
+    public static final ConfigKey<Duration> ENTITY_RECOVERED_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
+            .name("entityRecovered.stabilizationDelay")
+            .description("For a failed entity, time period for which the service must be consistently up for (e.g. doesn't report up-down-up) before emitting ENTITY_RECOVERED")
+            .defaultValue(Duration.ZERO)
+            .build();
+
+    @SetFromFlag("entityFailedRepublishTime")
+    public static final ConfigKey<Duration> ENTITY_FAILED_REPUBLISH_TIME = BasicConfigKey.builder(Duration.class)
+            .name("entityFailed.republishTime")
+            .description("Publish failed state periodically at the specified intervals, null to disable.")
+            .build();
+
+    protected Long firstUpTime;
+    
+    protected Long currentFailureStartTime = null;
+    protected Long currentRecoveryStartTime = null;
+    
+    protected Long publishEntityFailedTime = null;
+    protected Long publishEntityRecoveredTime = null;
+    protected Long setEntityOnFireTime = null;
+    
+    protected LastPublished lastPublished = LastPublished.NONE;
+
+    private final AtomicBoolean executorQueued = new AtomicBoolean(false);
+    private volatile long executorTime = 0;
+
+    /**
+     * TODO Really don't want this mutex!
+     * ServiceStateLogic.setExpectedState() will call into `onEvent(null)`, so could get concurrent calls.
+     * How to handle that? I don't think `ServiceStateLogic.setExpectedState` should be making the call, but
+     * presumably that is their to remove a race condition so it is set before method returns. Caller shouldn't
+     * rely on that though.
+     * e.g. see `ServiceFailureDetectorTest.testNotifiedOfFailureOnStateOnFire`, where we get two notifications.
+     */
+    private final Object mutex = new Object();
+    
+    public ServiceFailureDetector() {
+        this(new ConfigBag());
+    }
+    
+    public ServiceFailureDetector(Map<String,?> flags) {
+        this(new ConfigBag().putAll(flags));
+    }
+    
+    public ServiceFailureDetector(ConfigBag configBag) {
+        // TODO hierarchy should use ConfigBag, and not change flags
+        super(configBag.getAllConfigMutable());
+    }
+    
+    @Override
+    public void onEvent(SensorEvent<Object> event) {
+        if (firstUpTime==null) {
+            if (event!=null && Attributes.SERVICE_UP.equals(event.getSensor()) && Boolean.TRUE.equals(event.getValue())) {
+                firstUpTime = event.getTimestamp();
+            } else if (event == null && Boolean.TRUE.equals(entity.getAttribute(Attributes.SERVICE_UP))) {
+                // If this enricher is registered after the entity is up, then we'll get a "synthetic" onEvent(null) 
+                firstUpTime = System.currentTimeMillis();
+            }
+        }
+        
+        super.onEvent(event);
+    }
+    
+    @Override
+    protected void setActualState(Lifecycle state) {
+        long now = System.currentTimeMillis();
+
+        synchronized (mutex) {
+            if (state==Lifecycle.ON_FIRE) {
+                if (lastPublished == LastPublished.FAILED) {
+                    if (currentRecoveryStartTime != null) {
+                        if (LOG.isDebugEnabled()) LOG.debug("{} health-check for {}, component was recovering, now failing: {}", new Object[] {this, entity, getExplanation(state)});
+                        currentRecoveryStartTime = null;
+                        publishEntityRecoveredTime = null;
+                    } else {
+                        if (LOG.isTraceEnabled()) LOG.trace("{} health-check for {}, component still failed: {}", new Object[] {this, entity, getExplanation(state)});
+                    }
+                } else {
+                    if (firstUpTime == null && getConfig(ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP)) {
+                        // suppress; won't publish
+                    } else if (currentFailureStartTime == null) {
+                        if (LOG.isDebugEnabled()) LOG.debug("{} health-check for {}, component now failing: {}", new Object[] {this, entity, getExplanation(state)});
+                        currentFailureStartTime = now;
+                        publishEntityFailedTime = currentFailureStartTime + getConfig(ENTITY_FAILED_STABILIZATION_DELAY).toMilliseconds();
+                    } else {
+                        if (LOG.isTraceEnabled()) LOG.trace("{} health-check for {}, component continuing failing: {}", new Object[] {this, entity, getExplanation(state)});
+                    }
+                }
+                if (setEntityOnFireTime == null) {
+                    setEntityOnFireTime = now + getConfig(SERVICE_ON_FIRE_STABILIZATION_DELAY).toMilliseconds();
+                }
+                currentRecoveryStartTime = null;
+                publishEntityRecoveredTime = null;
+                
+            } else if (state == Lifecycle.RUNNING) {
+                if (lastPublished == LastPublished.FAILED) {
+                    if (currentRecoveryStartTime == null) {
+                        if (LOG.isDebugEnabled()) LOG.debug("{} health-check for {}, component now recovering: {}", new Object[] {this, entity, getExplanation(state)});
+                        currentRecoveryStartTime = now;
+                        publishEntityRecoveredTime = currentRecoveryStartTime + getConfig(ENTITY_RECOVERED_STABILIZATION_DELAY).toMilliseconds();
+                    } else {
+                        if (LOG.isTraceEnabled()) LOG.trace("{} health-check for {}, component continuing recovering: {}", new Object[] {this, entity, getExplanation(state)});
+                    }
+                } else {
+                    if (currentFailureStartTime != null) {
+                        if (LOG.isDebugEnabled()) LOG.debug("{} health-check for {}, component was failing, now healthy: {}", new Object[] {this, entity, getExplanation(state)});
+                    } else {
+                        if (LOG.isTraceEnabled()) LOG.trace("{} health-check for {}, component still healthy: {}", new Object[] {this, entity, getExplanation(state)});
+                    }
+                }
+                currentFailureStartTime = null;
+                publishEntityFailedTime = null;
+                setEntityOnFireTime = null;
+                
+            } else {
+                if (LOG.isTraceEnabled()) LOG.trace("{} health-check for {}, in unconfirmed sate: {}", new Object[] {this, entity, getExplanation(state)});
+            }
+
+            long recomputeIn = Long.MAX_VALUE; // For whether to call recomputeAfterDelay
+            
+            if (publishEntityFailedTime != null) {
+                long delayBeforeCheck = publishEntityFailedTime - now;
+                if (delayBeforeCheck<=0) {
+                    if (LOG.isDebugEnabled()) LOG.debug("{} publishing failed (state={}; currentFailureStartTime={}; now={}", 
+                            new Object[] {this, state, Time.makeDateString(currentFailureStartTime), Time.makeDateString(now)});
+                    Duration republishDelay = getConfig(ENTITY_FAILED_REPUBLISH_TIME);
+                    if (republishDelay == null) {
+                        publishEntityFailedTime = null;
+                    } else {
+                        publishEntityFailedTime = now + republishDelay.toMilliseconds();
+                        recomputeIn = Math.min(recomputeIn, republishDelay.toMilliseconds());
+                    }
+                    lastPublished = LastPublished.FAILED;
+                    entity.emit(HASensors.ENTITY_FAILED, new HASensors.FailureDescriptor(entity, getFailureDescription(now)));
+                } else {
+                    recomputeIn = Math.min(recomputeIn, delayBeforeCheck);
+                }
+            } else if (publishEntityRecoveredTime != null) {
+                long delayBeforeCheck = publishEntityRecoveredTime - now;
+                if (delayBeforeCheck<=0) {
+                    if (LOG.isDebugEnabled()) LOG.debug("{} publishing recovered (state={}; currentRecoveryStartTime={}; now={}", 
+                            new Object[] {this, state, Time.makeDateString(currentRecoveryStartTime), Time.makeDateString(now)});
+                    publishEntityRecoveredTime = null;
+                    lastPublished = LastPublished.RECOVERED;
+                    entity.emit(HASensors.ENTITY_RECOVERED, new HASensors.FailureDescriptor(entity, null));
+                } else {
+                    recomputeIn = Math.min(recomputeIn, delayBeforeCheck);
+                }
+            }
+            
+            if (setEntityOnFireTime != null) {
+                long delayBeforeCheck = setEntityOnFireTime - now;
+                if (delayBeforeCheck<=0) {
+                    if (LOG.isDebugEnabled()) LOG.debug("{} setting on-fire, now that deferred period has passed (state={})", 
+                            new Object[] {this, state});
+                    setEntityOnFireTime = null;
+                    super.setActualState(state);
+                } else {
+                    recomputeIn = Math.min(recomputeIn, delayBeforeCheck);
+                }
+            } else {
+                super.setActualState(state);
+            }
+            
+            if (recomputeIn < Long.MAX_VALUE) {
+                recomputeAfterDelay(recomputeIn);
+            }
+        }
+    }
+
+    protected String getExplanation(Lifecycle state) {
+        Duration serviceFailedStabilizationDelay = getConfig(ENTITY_FAILED_STABILIZATION_DELAY);
+        Duration serviceRecoveredStabilizationDelay = getConfig(ENTITY_RECOVERED_STABILIZATION_DELAY);
+
+        return String.format("location=%s; status=%s; lastPublished=%s; timeNow=%s; "+
+                    "currentFailurePeriod=%s; currentRecoveryPeriod=%s",
+                entity.getLocations(), 
+                (state != null ? state : "<unreported>"),
+                lastPublished,
+                Time.makeDateString(System.currentTimeMillis()),
+                (currentFailureStartTime != null ? getTimeStringSince(currentFailureStartTime) : "<none>") + " (stabilization "+Time.makeTimeStringRounded(serviceFailedStabilizationDelay) + ")",
+                (currentRecoveryStartTime != null ? getTimeStringSince(currentRecoveryStartTime) : "<none>") + " (stabilization "+Time.makeTimeStringRounded(serviceRecoveredStabilizationDelay) + ")");
+    }
+    
+    private String getFailureDescription(long now) {
+        String description = null;
+        Map<String, Object> serviceProblems = entity.getAttribute(Attributes.SERVICE_PROBLEMS);
+        if (serviceProblems!=null && !serviceProblems.isEmpty()) {
+            Entry<String, Object> problem = serviceProblems.entrySet().iterator().next();
+            description = problem.getKey()+": "+problem.getValue();
+            if (serviceProblems.size()>1) {
+                description = serviceProblems.size()+" service problems, including "+description;
+            } else {
+                description = "service problem: "+description;
+            }
+        } else if (Boolean.FALSE.equals(entity.getAttribute(Attributes.SERVICE_UP))) {
+            description = "service not up";
+        } else {
+            description = "service failure detected";
+        }
+        if (publishEntityFailedTime!=null && currentFailureStartTime!=null && publishEntityFailedTime > currentFailureStartTime)
+            description = " (stabilized for "+Duration.of(now - currentFailureStartTime, TimeUnit.MILLISECONDS)+")";
+        return description;
+    }
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    protected void recomputeAfterDelay(long delay) {
+        if (isRunning() && executorQueued.compareAndSet(false, true)) {
+            long now = System.currentTimeMillis();
+            delay = Math.max(0, Math.max(delay, (executorTime + MIN_PERIOD_BETWEEN_EXECS_MILLIS) - now));
+            if (LOG.isTraceEnabled()) LOG.trace("{} scheduling publish in {}ms", this, delay);
+            
+            Runnable job = new Runnable() {
+                @Override public void run() {
+                    try {
+                        executorTime = System.currentTimeMillis();
+                        executorQueued.set(false);
+
+                        onEvent(null);
+                        
+                    } catch (Exception e) {
+                        if (isRunning()) {
+                            LOG.error("Error in enricher "+this+": "+e, e);
+                        } else {
+                            if (LOG.isDebugEnabled()) LOG.debug("Error in enricher "+this+" (but no longer running): "+e, e);
+                        }
+                    } catch (Throwable t) {
+                        LOG.error("Error in enricher "+this+": "+t, t);
+                        throw Exceptions.propagate(t);
+                    }
+                }
+            };
+            
+            ScheduledTask task = new ScheduledTask(MutableMap.of("delay", Duration.of(delay, TimeUnit.MILLISECONDS)), new BasicTask(job));
+            ((EntityInternal)entity).getExecutionContext().submit(task);
+        }
+    }
+    
+    private String getTimeStringSince(Long time) {
+        return time == null ? null : Time.makeTimeStringRounded(System.currentTimeMillis() - time);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceReplacer.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceReplacer.java b/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceReplacer.java
new file mode 100644
index 0000000..d6989d9
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceReplacer.java
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.api.catalog.Catalog;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
+import org.apache.brooklyn.core.util.config.ConfigBag;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.ServiceStateLogic.ServiceProblemsLogic;
+import brooklyn.entity.group.StopFailedRuntimeException;
+import brooklyn.entity.trait.MemberReplaceable;
+import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.event.basic.BasicNotificationSensor;
+
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.Exceptions;
+
+import com.google.common.base.Ticker;
+import com.google.common.collect.Lists;
+
+/** attaches to a DynamicCluster and replaces a failed member in response to HASensors.ENTITY_FAILED or other sensor;
+ * if this fails, it sets the Cluster state to on-fire */
+@Catalog(name="Service Replacer", description="HA policy for replacing a failed member of a group")
+public class ServiceReplacer extends AbstractPolicy {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ServiceReplacer.class);
+
+    // TODO if there are multiple failures perhaps we should abort quickly
+    
+    public static final BasicNotificationSensor<FailureDescriptor> ENTITY_REPLACEMENT_FAILED = new BasicNotificationSensor<FailureDescriptor>(
+            FailureDescriptor.class, "ha.entityFailed.replacement", "Indicates that an entity replacement attempt has failed");
+
+    @SetFromFlag("setOnFireOnFailure")
+    public static final ConfigKey<Boolean> SET_ON_FIRE_ON_FAILURE = ConfigKeys.newBooleanConfigKey("setOnFireOnFailure", "", true);
+    
+    /** monitors this sensor, by default ENTITY_RESTART_FAILED */
+    @SetFromFlag("failureSensorToMonitor")
+    @SuppressWarnings("rawtypes")
+    public static final ConfigKey<Sensor> FAILURE_SENSOR_TO_MONITOR = new BasicConfigKey<Sensor>(Sensor.class, "failureSensorToMonitor", "", ServiceRestarter.ENTITY_RESTART_FAILED); 
+
+    /** skips replace if replacement has failed this many times failure re-occurs within this time interval */
+    @SetFromFlag("failOnRecurringFailuresInThisDuration")
+    public static final ConfigKey<Long> FAIL_ON_RECURRING_FAILURES_IN_THIS_DURATION = ConfigKeys.newLongConfigKey(
+            "failOnRecurringFailuresInThisDuration", 
+            "abandon replace if replacement has failed many times within this time interval",
+            5*60*1000L);
+
+    /** skips replace if replacement has failed this many times failure re-occurs within this time interval */
+    @SetFromFlag("failOnNumRecurringFailures")
+    public static final ConfigKey<Integer> FAIL_ON_NUM_RECURRING_FAILURES = ConfigKeys.newIntegerConfigKey(
+            "failOnNumRecurringFailures", 
+            "abandon replace if replacement has failed this many times (100% of attempts) within the time interval",
+            5);
+
+    @SetFromFlag("ticker")
+    public static final ConfigKey<Ticker> TICKER = ConfigKeys.newConfigKey(Ticker.class,
+            "ticker", 
+            "A time source (defaults to system-clock, which is almost certainly what's wanted, except in tests)",
+            null);
+
+    protected final List<Long> consecutiveReplacementFailureTimes = Lists.newCopyOnWriteArrayList();
+    
+    public ServiceReplacer() {
+        this(new ConfigBag());
+    }
+    
+    public ServiceReplacer(Map<String,?> flags) {
+        this(new ConfigBag().putAll(flags));
+    }
+    
+    public ServiceReplacer(ConfigBag configBag) {
+        // TODO hierarchy should use ConfigBag, and not change flags
+        super(configBag.getAllConfigMutable());
+    }
+    
+    public ServiceReplacer(Sensor<?> failureSensorToMonitor) {
+        this(new ConfigBag().configure(FAILURE_SENSOR_TO_MONITOR, failureSensorToMonitor));
+    }
+
+    @Override
+    public void setEntity(final EntityLocal entity) {
+        checkArgument(entity instanceof MemberReplaceable, "ServiceReplacer must take a MemberReplaceable, not %s", entity);
+        Sensor<?> failureSensorToMonitor = checkNotNull(getConfig(FAILURE_SENSOR_TO_MONITOR), "failureSensorToMonitor");
+        
+        super.setEntity(entity);
+
+        subscribeToMembers((Group)entity, failureSensorToMonitor, new SensorEventListener<Object>() {
+                @Override public void onEvent(final SensorEvent<Object> event) {
+                    // Must execute in another thread - if we called entity.replaceMember in the event-listener's thread
+                    // then we'd block all other events being delivered to this entity's other subscribers.
+                    // Relies on synchronization of `onDetectedFailure`.
+                    // See same pattern used in ServiceRestarter.
+                    
+                    // TODO Could use BasicExecutionManager.setTaskSchedulerForTag to prevent race of two
+                    // events being received in rapid succession, and onDetectedFailure being executed out-of-order
+                    // for them; or could write events to a blocking queue and have onDetectedFailure read from that.
+                    
+                    if (isRunning()) {
+                        LOG.warn("ServiceReplacer notified; dispatching job for "+entity+" ("+event.getValue()+")");
+                        ((EntityInternal)entity).getExecutionContext().submit(MutableMap.of(), new Runnable() {
+                            @Override public void run() {
+                                onDetectedFailure(event);
+                            }});
+                    } else {
+                        LOG.warn("ServiceReplacer not running, so not acting on failure detected at "+entity+" ("+event.getValue()+", child of "+entity+")");
+                    }
+                }
+            });
+    }
+    
+    // TODO semaphores would be better to allow at-most-one-blocking behaviour
+    protected synchronized void onDetectedFailure(SensorEvent<Object> event) {
+        final Entity failedEntity = event.getSource();
+        final Object reason = event.getValue();
+        
+        if (isSuspended()) {
+            LOG.warn("ServiceReplacer suspended, so not acting on failure detected at "+failedEntity+" ("+reason+", child of "+entity+")");
+            return;
+        }
+
+        if (isRepeatedlyFailingTooMuch()) {
+            LOG.error("ServiceReplacer not acting on failure detected at "+failedEntity+" ("+reason+", child of "+entity+"), because too many recent replacement failures");
+            return;
+        }
+        
+        LOG.warn("ServiceReplacer acting on failure detected at "+failedEntity+" ("+reason+", child of "+entity+")");
+        ((EntityInternal)entity).getManagementSupport().getExecutionContext().submit(MutableMap.of(), new Runnable() {
+
+            @Override
+            public void run() {
+                try {
+                    Entities.invokeEffectorWithArgs(entity, entity, MemberReplaceable.REPLACE_MEMBER, failedEntity.getId()).get();
+                    consecutiveReplacementFailureTimes.clear();
+                } catch (Exception e) {
+                    if (Exceptions.getFirstThrowableOfType(e, StopFailedRuntimeException.class) != null) {
+                        LOG.info("ServiceReplacer: ignoring error reported from stopping failed node "+failedEntity);
+                        return;
+                    }
+                    onReplacementFailed("Replace failure ("+Exceptions.collapseText(e)+") at "+entity+": "+reason);
+                }
+            }
+        });
+    }
+
+    private boolean isRepeatedlyFailingTooMuch() {
+        Integer failOnNumRecurringFailures = getConfig(FAIL_ON_NUM_RECURRING_FAILURES);
+        long failOnRecurringFailuresInThisDuration = getConfig(FAIL_ON_RECURRING_FAILURES_IN_THIS_DURATION);
+        long oldestPermitted = currentTimeMillis() - failOnRecurringFailuresInThisDuration;
+        
+        // trim old ones
+        for (Iterator<Long> iter = consecutiveReplacementFailureTimes.iterator(); iter.hasNext();) {
+            Long timestamp = iter.next();
+            if (timestamp < oldestPermitted) {
+                iter.remove();
+            } else {
+                break;
+            }
+        }
+        
+        return (consecutiveReplacementFailureTimes.size() >= failOnNumRecurringFailures);
+    }
+
+    protected long currentTimeMillis() {
+        Ticker ticker = getConfig(TICKER);
+        return (ticker == null) ? System.currentTimeMillis() : TimeUnit.NANOSECONDS.toMillis(ticker.read());
+    }
+    
+    protected void onReplacementFailed(String msg) {
+        LOG.warn("ServiceReplacer failed for "+entity+": "+msg);
+        consecutiveReplacementFailureTimes.add(currentTimeMillis());
+        
+        if (getConfig(SET_ON_FIRE_ON_FAILURE)) {
+            ServiceProblemsLogic.updateProblemsIndicator(entity, "ServiceReplacer", "replacement failed: "+msg);
+        }
+        entity.emit(ENTITY_REPLACEMENT_FAILED, new FailureDescriptor(entity, msg));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceRestarter.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceRestarter.java b/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceRestarter.java
new file mode 100644
index 0000000..82f8664
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/ha/ServiceRestarter.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.api.catalog.Catalog;
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
+import org.apache.brooklyn.core.util.config.ConfigBag;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.basic.BasicNotificationSensor;
+
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.javalang.JavaClassNames;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Preconditions;
+
+/** attaches to a SoftwareProcess (or anything Startable, emitting ENTITY_FAILED or other configurable sensor),
+ * and invokes restart on failure; 
+ * if there is a subsequent failure within a configurable time interval, or if the restart fails,
+ * this gives up and emits {@link #ENTITY_RESTART_FAILED} 
+ */
+@Catalog(name="Service Restarter", description="HA policy for restarting a service automatically, "
+        + "and for emitting an events if the service repeatedly fails")
+public class ServiceRestarter extends AbstractPolicy {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ServiceRestarter.class);
+
+    public static final BasicNotificationSensor<FailureDescriptor> ENTITY_RESTART_FAILED = new BasicNotificationSensor<FailureDescriptor>(
+            FailureDescriptor.class, "ha.entityFailed.restart", "Indicates that an entity restart attempt has failed");
+
+    /** skips retry if a failure re-occurs within this time interval */
+    @SetFromFlag("failOnRecurringFailuresInThisDuration")
+    public static final ConfigKey<Duration> FAIL_ON_RECURRING_FAILURES_IN_THIS_DURATION = ConfigKeys.newConfigKey(
+            Duration.class, 
+            "failOnRecurringFailuresInThisDuration", 
+            "Reports entity as failed if it fails two or more times in this time window", 
+            Duration.minutes(3));
+
+    @SetFromFlag("setOnFireOnFailure")
+    public static final ConfigKey<Boolean> SET_ON_FIRE_ON_FAILURE = ConfigKeys.newBooleanConfigKey("setOnFireOnFailure", "", true);
+
+    /** monitors this sensor, by default ENTITY_FAILED */
+    @SetFromFlag("failureSensorToMonitor")
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public static final ConfigKey<Sensor<?>> FAILURE_SENSOR_TO_MONITOR = (ConfigKey) ConfigKeys.newConfigKey(Sensor.class, "failureSensorToMonitor", "", HASensors.ENTITY_FAILED); 
+    
+    protected final AtomicReference<Long> lastFailureTime = new AtomicReference<Long>();
+
+    public ServiceRestarter() {
+        this(new ConfigBag());
+    }
+    
+    public ServiceRestarter(Map<String,?> flags) {
+        this(new ConfigBag().putAll(flags));
+    }
+    
+    public ServiceRestarter(ConfigBag configBag) {
+        // TODO hierarchy should use ConfigBag, and not change flags
+        super(configBag.getAllConfigMutable());
+        uniqueTag = JavaClassNames.simpleClassName(getClass())+":"+getConfig(FAILURE_SENSOR_TO_MONITOR).getName();
+    }
+    
+    public ServiceRestarter(Sensor<?> failureSensorToMonitor) {
+        this(new ConfigBag().configure(FAILURE_SENSOR_TO_MONITOR, failureSensorToMonitor));
+    }
+
+    @Override
+    public void setEntity(final EntityLocal entity) {
+        Preconditions.checkArgument(entity instanceof Startable, "Restarter must take a Startable, not "+entity);
+        
+        super.setEntity(entity);
+        
+        subscribe(entity, getConfig(FAILURE_SENSOR_TO_MONITOR), new SensorEventListener<Object>() {
+                @Override public void onEvent(final SensorEvent<Object> event) {
+                    // Must execute in another thread - if we called entity.restart in the event-listener's thread
+                    // then we'd block all other events being delivered to this entity's other subscribers.
+                    // Relies on synchronization of `onDetectedFailure`.
+                    // See same pattern used in ServiceReplacer.
+
+                    // TODO Could use BasicExecutionManager.setTaskSchedulerForTag to prevent race of two
+                    // events being received in rapid succession, and onDetectedFailure being executed out-of-order
+                    // for them; or could write events to a blocking queue and have onDetectedFailure read from that.
+                    
+                    if (isRunning()) {
+                        LOG.info("ServiceRestarter notified; dispatching job for "+entity+" ("+event.getValue()+")");
+                        ((EntityInternal)entity).getExecutionContext().submit(MutableMap.of(), new Runnable() {
+                            @Override public void run() {
+                                onDetectedFailure(event);
+                            }});
+                    } else {
+                        LOG.warn("ServiceRestarter not running, so not acting on failure detected at "+entity+" ("+event.getValue()+")");
+                    }
+                }
+            });
+    }
+    
+    // TODO semaphores would be better to allow at-most-one-blocking behaviour
+    // FIXME as this is called in message-dispatch (single threaded) we should do most of this in a new submitted task
+    // (as has been done in ServiceReplacer)
+    protected synchronized void onDetectedFailure(SensorEvent<Object> event) {
+        if (isSuspended()) {
+            LOG.warn("ServiceRestarter suspended, so not acting on failure detected at "+entity+" ("+event.getValue()+")");
+            return;
+        }
+
+        LOG.warn("ServiceRestarter acting on failure detected at "+entity+" ("+event.getValue()+")");
+        long current = System.currentTimeMillis();
+        Long last = lastFailureTime.getAndSet(current);
+        long elapsed = last==null ? -1 : current-last;
+        if (elapsed>=0 && elapsed <= getConfig(FAIL_ON_RECURRING_FAILURES_IN_THIS_DURATION).toMilliseconds()) {
+            onRestartFailed("Restart failure (failed again after "+Time.makeTimeStringRounded(elapsed)+") at "+entity+": "+event.getValue());
+            return;
+        }
+        try {
+            ServiceStateLogic.setExpectedState(entity, Lifecycle.STARTING);
+            Entities.invokeEffector(entity, entity, Startable.RESTART).get();
+        } catch (Exception e) {
+            onRestartFailed("Restart failure (error "+e+") at "+entity+": "+event.getValue());
+        }
+    }
+
+    protected void onRestartFailed(String msg) {
+        LOG.warn("ServiceRestarter failed for "+entity+": "+msg);
+        if (getConfig(SET_ON_FIRE_ON_FAILURE)) {
+            ServiceStateLogic.setExpectedState(entity, Lifecycle.ON_FIRE);
+        }
+        entity.emit(ENTITY_RESTART_FAILED, new FailureDescriptor(entity, msg));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/ha/SshMachineFailureDetector.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/ha/SshMachineFailureDetector.java b/policy/src/main/java/org/apache/brooklyn/policy/ha/SshMachineFailureDetector.java
new file mode 100644
index 0000000..e679309
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/ha/SshMachineFailureDetector.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.api.catalog.Catalog;
+import org.apache.brooklyn.core.util.internal.ssh.SshTool;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.event.basic.BasicNotificationSensor;
+
+import org.apache.brooklyn.location.basic.Machines;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.guava.Maybe;
+import brooklyn.util.time.Duration;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+@Catalog(name="Ssh Connectivity Failure Detector", description="HA policy for monitoring an SshMachine, "
+        + "emitting an event if the connection is lost/restored")
+public class SshMachineFailureDetector extends AbstractFailureDetector {
+    private static final Logger LOG = LoggerFactory.getLogger(SshMachineFailureDetector.class);
+    public static final String DEFAULT_UNIQUE_TAG = "failureDetector.sshMachine.tag";
+
+    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_FAILED = HASensors.CONNECTION_FAILED;
+
+    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_RECOVERED = HASensors.CONNECTION_RECOVERED;
+
+    public static final ConfigKey<Duration> CONNECT_TIMEOUT = ConfigKeys.newDurationConfigKey(
+            "ha.sshConnection.timeout", "How long to wait for conneciton before declaring failure", Duration.TEN_SECONDS);
+
+    @Override
+    public void init() {
+        super.init();
+        if (config().getRaw(SENSOR_FAILED).isAbsent()) {
+            config().set(SENSOR_FAILED, CONNECTION_FAILED);
+        }
+        if (config().getRaw(SENSOR_RECOVERED).isAbsent()) {
+            config().set(SENSOR_RECOVERED, CONNECTION_RECOVERED);
+        }
+        if (config().getRaw(POLL_PERIOD).isAbsent()) {
+            config().set(POLL_PERIOD, Duration.ONE_MINUTE);
+        }
+        uniqueTag = DEFAULT_UNIQUE_TAG;
+    }
+
+    @Override
+    protected CalculatedStatus calculateStatus() {
+        Maybe<SshMachineLocation> sshMachineOption = Machines.findUniqueSshMachineLocation(entity.getLocations());
+        if (sshMachineOption.isPresent()) {
+            SshMachineLocation sshMachine = sshMachineOption.get();
+            try {
+                Duration timeout = config().get(CONNECT_TIMEOUT);
+                Map<String, ?> flags = ImmutableMap.of(
+                        SshTool.PROP_CONNECT_TIMEOUT.getName(), timeout.toMilliseconds(),
+                        SshTool.PROP_SESSION_TIMEOUT.getName(), timeout.toMilliseconds(),
+                        SshTool.PROP_SSH_TRIES.getName(), 1);
+                int exitCode = sshMachine.execCommands(flags, SshMachineFailureDetector.class.getName(), ImmutableList.of("exit"));
+                return new BasicCalculatedStatus(exitCode == 0, sshMachine.toString());
+            } catch (Exception e) {
+                Exceptions.propagateIfFatal(e);
+                boolean isFirstFailure = lastPublished != LastPublished.FAILED && currentFailureStartTime == null;
+                if (isFirstFailure) {
+                    if (LOG.isDebugEnabled()) {
+                        LOG.debug("Failed connecting to machine " + sshMachine, e);
+                    }
+                } else {
+                    if (LOG.isTraceEnabled()) {
+                        LOG.trace("Failed connecting to machine " + sshMachine, e);
+                    }
+                }
+                return new BasicCalculatedStatus(false, e.getMessage());
+            }
+        } else {
+            return new BasicCalculatedStatus(true, "no machine started, not complaining");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableContainer.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableContainer.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableContainer.java
new file mode 100644
index 0000000..451d150
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableContainer.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.AbstractGroup;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.event.basic.BasicNotificationSensor;
+import brooklyn.util.collections.QuorumCheck;
+import brooklyn.util.collections.QuorumCheck.QuorumChecks;
+
+/**
+ * Contains worker items that can be moved between this container and others to effect load balancing.
+ * Membership of a balanceable container does not imply a parent-child relationship in the Brooklyn
+ * management sense.
+ */
+public interface BalanceableContainer<ItemType extends Movable> extends Entity, AbstractGroup {
+    
+    public static BasicNotificationSensor<Entity> ITEM_ADDED = new BasicNotificationSensor<Entity>(
+            Entity.class, "balanceablecontainer.item.added", "Movable item added to balanceable container");
+    public static BasicNotificationSensor<Entity> ITEM_REMOVED = new BasicNotificationSensor<Entity>(
+            Entity.class, "balanceablecontainer.item.removed", "Movable item removed from balanceable container");
+    
+    public static final ConfigKey<QuorumCheck> UP_QUORUM_CHECK = ConfigKeys.newConfigKeyWithDefault(AbstractGroup.UP_QUORUM_CHECK, 
+        "Up check from members; default one for container overrides usual check to always return true, "
+        + "i.e. not block service up simply because the container is empty or something in the container has failed",
+        QuorumChecks.alwaysTrue());
+
+    public Set<ItemType> getBalanceableItems();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceablePoolModel.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceablePoolModel.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceablePoolModel.java
new file mode 100644
index 0000000..72793b1
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceablePoolModel.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.location.Location;
+
+/**
+ * Captures the state of a balanceable cluster of containers and all their constituent items, including workrates,
+ * for consumption by a {@link BalancingStrategy}.
+ */
+public interface BalanceablePoolModel<ContainerType, ItemType> {
+    
+    // Attributes of the pool.
+    public String getName();
+    public int getPoolSize();
+    public Set<ContainerType> getPoolContents();
+    public double getPoolLowThreshold();
+    public double getPoolHighThreshold();
+    public double getCurrentPoolWorkrate();
+    public boolean isHot();
+    public boolean isCold();
+    
+    
+    // Attributes of containers and items.
+    public String getName(ContainerType container);
+    public Location getLocation(ContainerType container);
+    public double getLowThreshold(ContainerType container); // -1 for not known / invalid
+    public double getHighThreshold(ContainerType container); // -1 for not known / invalid
+    public double getTotalWorkrate(ContainerType container); // -1 for not known / invalid
+    public Map<ContainerType, Double> getContainerWorkrates(); // contains -1 for items which are unknown
+    /** contains -1 instead of actual item workrate, for items which cannot be moved */
+    // @Nullable("null if the node is prevented from reporting and/or being adjusted, or has no data yet")
+    public Map<ItemType, Double> getItemWorkrates(ContainerType container);
+    public boolean isItemMoveable(ItemType item);
+    public boolean isItemAllowedIn(ItemType item, Location location);
+    
+    // Mutators for keeping the model in-sync with the observed world
+    public void onContainerAdded(ContainerType newContainer, double lowThreshold, double highThreshold);
+    public void onContainerRemoved(ContainerType oldContainer);
+    public void onItemAdded(ItemType item, ContainerType parentContainer);
+    public void onItemAdded(ItemType item, ContainerType parentContainer, boolean immovable);
+    public void onItemRemoved(ItemType item);
+    public void onItemWorkrateUpdated(ItemType item, double newValue);
+    public void onItemMoved(ItemType item, ContainerType targetContainer);
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPool.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPool.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPool.java
new file mode 100644
index 0000000..d600bc4
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPool.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.Serializable;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
+
+import brooklyn.entity.trait.Resizable;
+import brooklyn.event.basic.BasicNotificationSensor;
+
+/**
+ * Represents an elastic group of "container" entities, each of which is capable of hosting "item" entities that perform
+ * work and consume the container's available resources (e.g. CPU or bandwidth). Auto-scaling and load-balancing policies can
+ * be attached to this pool to provide dynamic elasticity based on workrates reported by the individual item entities.
+ * <p>
+ * The containers must be "up" in order to receive work, thus they must NOT follow the default enricher pattern
+ * for groups which says that the group must be up to receive work.
+ */
+@ImplementedBy(BalanceableWorkerPoolImpl.class)
+public interface BalanceableWorkerPool extends Entity, Resizable {
+
+    // FIXME Asymmetry between loadbalancing and followTheSun: ITEM_ADDED and ITEM_REMOVED in loadbalancing
+    // are of type ContainerItemPair, but in followTheSun it is just the `Entity item`.
+    
+    /** Encapsulates an item and a container; emitted for {@code ITEM_ADDED}, {@code ITEM_REMOVED} and
+     * {@code ITEM_MOVED} sensors.
+     */
+    public static class ContainerItemPair implements Serializable {
+        private static final long serialVersionUID = 1L;
+        public final BalanceableContainer<?> container;
+        public final Entity item;
+        
+        public ContainerItemPair(BalanceableContainer<?> container, Entity item) {
+            this.container = container;
+            this.item = checkNotNull(item);
+        }
+        
+        @Override
+        public String toString() {
+            return ""+item+" @ "+container;
+        }
+    }
+    
+    // Pool constituent notifications.
+    public static BasicNotificationSensor<Entity> CONTAINER_ADDED = new BasicNotificationSensor<Entity>(
+        Entity.class, "balanceablepool.container.added", "Container added to balanceable pool");
+    public static BasicNotificationSensor<Entity> CONTAINER_REMOVED = new BasicNotificationSensor<Entity>(
+        Entity.class, "balanceablepool.container.removed", "Container removed from balanceable pool");
+    public static BasicNotificationSensor<ContainerItemPair> ITEM_ADDED = new BasicNotificationSensor<ContainerItemPair>(
+        ContainerItemPair.class, "balanceablepool.item.added", "Item added to balanceable pool");
+    public static BasicNotificationSensor<ContainerItemPair> ITEM_REMOVED = new BasicNotificationSensor<ContainerItemPair>(
+        ContainerItemPair.class, "balanceablepool.item.removed", "Item removed from balanceable pool");
+    public static BasicNotificationSensor<ContainerItemPair> ITEM_MOVED = new BasicNotificationSensor<ContainerItemPair>(
+        ContainerItemPair.class, "balanceablepool.item.moved", "Item moved in balanceable pool to the given container");
+    
+    public void setResizable(Resizable resizable);
+    
+    public void setContents(Group containerGroup, Group itemGroup);
+    
+    public Group getContainerGroup();
+    
+    public Group getItemGroup();
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPoolImpl.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPoolImpl.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPoolImpl.java
new file mode 100644
index 0000000..888b957
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPoolImpl.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.AbstractEntity;
+import brooklyn.entity.basic.AbstractGroup;
+import brooklyn.entity.trait.Resizable;
+import brooklyn.entity.trait.Startable;
+
+/**
+ * @see BalanceableWorkerPool
+ */
+public class BalanceableWorkerPoolImpl extends AbstractEntity implements BalanceableWorkerPool {
+
+    // FIXME Asymmetry between loadbalancing and followTheSun: ITEM_ADDED and ITEM_REMOVED in loadbalancing
+    // are of type ContainerItemPair, but in followTheSun it is just the `Entity item`.
+    
+    private static final Logger LOG = LoggerFactory.getLogger(BalanceableWorkerPool.class);
+    
+    private Group containerGroup;
+    private Group itemGroup;
+    private Resizable resizable;
+    
+    private final Set<Entity> containers = Collections.synchronizedSet(new HashSet<Entity>());
+    private final Set<Entity> items = Collections.synchronizedSet(new HashSet<Entity>());
+    
+    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
+        @Override
+        public void onEvent(SensorEvent<Object> event) {
+            if (LOG.isTraceEnabled()) LOG.trace("{} received event {}", BalanceableWorkerPoolImpl.this, event);
+            Entity source = event.getSource();
+            Object value = event.getValue();
+            Sensor<?> sensor = event.getSensor();
+            
+            if (sensor.equals(AbstractGroup.MEMBER_ADDED)) {
+                if (source.equals(containerGroup)) {
+                    onContainerAdded((BalanceableContainer<?>) value);
+                } else if (source.equals(itemGroup)) {
+                    onItemAdded((Entity)value);
+                } else {
+                    throw new IllegalStateException("unexpected event source="+source);
+                }
+            } else if (sensor.equals(AbstractGroup.MEMBER_REMOVED)) {
+                if (source.equals(containerGroup)) {
+                    onContainerRemoved((BalanceableContainer<?>) value);
+                } else if (source.equals(itemGroup)) {
+                    onItemRemoved((Entity) value);
+                } else {
+                    throw new IllegalStateException("unexpected event source="+source);
+                }
+            } else if (sensor.equals(Startable.SERVICE_UP)) {
+                // TODO What if start has failed? Is there a sensor to indicate that?
+                if ((Boolean)value) {
+                    onContainerUp((BalanceableContainer<?>) source);
+                } else {
+                    onContainerDown((BalanceableContainer<?>) source);
+                }
+            } else if (sensor.equals(Movable.CONTAINER)) {
+                onItemMoved(source, (BalanceableContainer<?>) value);
+            } else {
+                throw new IllegalStateException("Unhandled event type "+sensor+": "+event);
+            }
+        }
+    };
+    
+    public BalanceableWorkerPoolImpl() {
+    }
+
+    @Override
+    public void setResizable(Resizable resizable) {
+        this.resizable = resizable;
+    }
+    
+    @Override
+    public void setContents(Group containerGroup, Group itemGroup) {
+        this.containerGroup = containerGroup;
+        this.itemGroup = itemGroup;
+        if (resizable == null && containerGroup instanceof Resizable) resizable = (Resizable) containerGroup;
+        
+        subscribe(containerGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
+        subscribe(containerGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
+        subscribe(itemGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
+        subscribe(itemGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
+        
+        // Process extant containers and items
+        for (Entity existingContainer : containerGroup.getMembers()) {
+            onContainerAdded((BalanceableContainer<?>)existingContainer);
+        }
+        for (Entity existingItem : itemGroup.getMembers()) {
+            onItemAdded(existingItem);
+        }
+    }
+    
+    @Override
+    public Group getContainerGroup() {
+        return containerGroup;
+    }
+    
+    @Override
+    public Group getItemGroup() {
+        return itemGroup;
+    }
+
+    @Override
+    public Integer getCurrentSize() {
+        return containerGroup.getCurrentSize();
+    }
+    
+    @Override
+    public Integer resize(Integer desiredSize) {
+        if (resizable != null) return resizable.resize(desiredSize);
+        
+        throw new UnsupportedOperationException("Container group is not resizable, and no resizable supplied: "+containerGroup+" of type "+(containerGroup != null ? containerGroup.getClass().getCanonicalName() : null));
+    }
+    
+    private void onContainerAdded(BalanceableContainer<?> newContainer) {
+        subscribe(newContainer, Startable.SERVICE_UP, eventHandler);
+        if (!(newContainer instanceof Startable) || Boolean.TRUE.equals(newContainer.getAttribute(Startable.SERVICE_UP))) {
+            onContainerUp(newContainer);
+        }
+    }
+    
+    private void onContainerUp(BalanceableContainer<?> newContainer) {
+        if (containers.add(newContainer)) {
+            emit(CONTAINER_ADDED, newContainer);
+        }
+    }
+    
+    private void onContainerDown(BalanceableContainer<?> oldContainer) {
+        if (containers.remove(oldContainer)) {
+            emit(CONTAINER_REMOVED, oldContainer);
+        }
+    }
+    
+    private void onContainerRemoved(BalanceableContainer<?> oldContainer) {
+        unsubscribe(oldContainer);
+        onContainerDown(oldContainer);
+    }
+    
+    private void onItemAdded(Entity item) {
+        if (items.add(item)) {
+            subscribe(item, Movable.CONTAINER, eventHandler);
+            emit(ITEM_ADDED, new ContainerItemPair(item.getAttribute(Movable.CONTAINER), item));
+        }
+    }
+    
+    private void onItemRemoved(Entity item) {
+        if (items.remove(item)) {
+            unsubscribe(item);
+            emit(ITEM_REMOVED, new ContainerItemPair(null, item));
+        }
+    }
+    
+    private void onItemMoved(Entity item, BalanceableContainer<?> container) {
+        emit(ITEM_MOVED, new ContainerItemPair(container, item));
+    }
+}


[05/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockItemEntityImpl.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockItemEntityImpl.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockItemEntityImpl.java
new file mode 100644
index 0000000..00b665d
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockItemEntityImpl.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.event.AttributeSensor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.AbstractEntity;
+import brooklyn.util.collections.MutableList;
+
+
+public class MockItemEntityImpl extends AbstractEntity implements MockItemEntity {
+
+    private static final Logger LOG = LoggerFactory.getLogger(MockItemEntityImpl.class);
+    
+    public static AtomicInteger totalMoveCount = new AtomicInteger(0);
+    
+    public static AtomicLong lastMoveTime = new AtomicLong(-1);
+    
+    private volatile boolean stopped;
+    private volatile MockContainerEntity currentContainer;
+    
+    private final ReentrantLock _lock = new ReentrantLock();
+    
+    @Override
+    public String getContainerId() {
+        return (currentContainer == null) ? null : currentContainer.getId();
+    }
+
+    @Override
+    public boolean isStopped() {
+        return stopped;
+    }
+
+    @Override
+    public <T> T setAttribute(AttributeSensor<T> attribute, T val) {
+        if (LOG.isDebugEnabled()) LOG.debug("Mocks: item {} setting {} to {}", new Object[] {this, attribute, val});
+        return super.setAttribute(attribute, val);
+    }
+
+    @Override
+    public void move(Entity destination) {
+        totalMoveCount.incrementAndGet();
+        lastMoveTime.set(System.currentTimeMillis());
+        moveNonEffector(destination);
+    }
+    
+    // only moves if the containers will accept us (otherwise we'd lose the item!)
+    @Override
+    public void moveNonEffector(Entity rawDestination) {
+        if (LOG.isDebugEnabled()) LOG.debug("Mocks: moving item {} from {} to {}", new Object[] {this, currentContainer, rawDestination});
+        checkNotNull(rawDestination);
+        final MockContainerEntity previousContainer = currentContainer;
+        final MockContainerEntity destination = (MockContainerEntity) rawDestination;
+        
+        MockContainerEntityImpl.runWithLock(MutableList.of(previousContainer, destination), new Runnable() { 
+            @Override public void run() {
+                _lock.lock();
+                try {
+                    if (stopped) throw new IllegalStateException("Item "+this+" is stopped; cannot move to "+destination);
+                    if (currentContainer != null) currentContainer.removeItem(MockItemEntityImpl.this);
+                    currentContainer = destination;
+                    destination.addItem(MockItemEntityImpl.this);
+                    setAttribute(CONTAINER, currentContainer);
+                } finally {
+                    _lock.unlock();
+                }
+        }});
+    }
+    
+    @Override
+    public void stop() {
+        // FIXME How best to indicate this has been entirely stopped, rather than just in-transit?
+        if (LOG.isDebugEnabled()) LOG.debug("Mocks: stopping item {} (was in container {})", this, currentContainer);
+        _lock.lock();
+        try {
+            if (currentContainer != null) currentContainer.removeItem(this);
+            currentContainer = null;
+            stopped = true;
+        } finally {
+            _lock.unlock();
+        }
+    }
+    
+    @Override
+    public String toString() {
+        return "MockItem["+getDisplayName()+"]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/TomcatAutoScalerPolicyTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/TomcatAutoScalerPolicyTest.java b/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/TomcatAutoScalerPolicyTest.java
index acf6195..9bdd2c6 100644
--- a/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/TomcatAutoScalerPolicyTest.java
+++ b/software/webapp/src/test/java/org/apache/brooklyn/entity/webapp/TomcatAutoScalerPolicyTest.java
@@ -25,7 +25,6 @@ import static org.testng.Assert.assertTrue;
 import org.apache.brooklyn.api.entity.proxying.EntitySpec;
 import org.apache.brooklyn.api.location.LocationSpec;
 import org.apache.brooklyn.api.management.ManagementContext;
-import org.apache.brooklyn.entity.webapp.DynamicWebAppCluster;
 import org.apache.brooklyn.entity.webapp.tomcat.TomcatServer;
 import org.apache.brooklyn.entity.webapp.tomcat.TomcatServerImpl;
 import org.apache.brooklyn.test.entity.TestApplication;
@@ -39,7 +38,7 @@ import brooklyn.entity.basic.Entities;
 
 import org.apache.brooklyn.location.basic.LocalhostMachineProvisioningLocation;
 
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
 import brooklyn.test.Asserts;
 import brooklyn.util.collections.MutableMap;
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/archetypes/quickstart/src/brooklyn-sample/src/main/java/com/acme/sample/brooklyn/sample/app/ClusterWebServerDatabaseSample.java
----------------------------------------------------------------------
diff --git a/usage/archetypes/quickstart/src/brooklyn-sample/src/main/java/com/acme/sample/brooklyn/sample/app/ClusterWebServerDatabaseSample.java b/usage/archetypes/quickstart/src/brooklyn-sample/src/main/java/com/acme/sample/brooklyn/sample/app/ClusterWebServerDatabaseSample.java
index c9b73e0..9b101a8 100644
--- a/usage/archetypes/quickstart/src/brooklyn-sample/src/main/java/com/acme/sample/brooklyn/sample/app/ClusterWebServerDatabaseSample.java
+++ b/usage/archetypes/quickstart/src/brooklyn-sample/src/main/java/com/acme/sample/brooklyn/sample/app/ClusterWebServerDatabaseSample.java
@@ -30,7 +30,7 @@ import org.apache.brooklyn.entity.webapp.WebAppServiceConstants;
 import org.apache.brooklyn.api.event.AttributeSensor;
 import brooklyn.event.basic.Sensors;
 import org.apache.brooklyn.location.basic.PortRanges;
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
 import brooklyn.util.maven.MavenArtifact;
 import brooklyn.util.maven.MavenRetriever;
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/BrooklynYamlTypeInstantiatorTest.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/BrooklynYamlTypeInstantiatorTest.java b/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/BrooklynYamlTypeInstantiatorTest.java
index 107b0b9..7999721 100644
--- a/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/BrooklynYamlTypeInstantiatorTest.java
+++ b/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/BrooklynYamlTypeInstantiatorTest.java
@@ -27,7 +27,7 @@ import org.apache.brooklyn.core.management.classloading.JavaBrooklynClassLoading
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import brooklyn.policy.ha.ServiceRestarter;
+import org.apache.brooklyn.policy.ha.ServiceRestarter;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.javalang.JavaClassNames;
 import brooklyn.util.time.Duration;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/JavaWebAppsIntegrationTest.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/JavaWebAppsIntegrationTest.java b/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/JavaWebAppsIntegrationTest.java
index 7314e66..5a6d7d8 100644
--- a/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/JavaWebAppsIntegrationTest.java
+++ b/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/JavaWebAppsIntegrationTest.java
@@ -36,9 +36,11 @@ import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.management.ManagementContext;
 import org.apache.brooklyn.api.management.Task;
 import org.apache.brooklyn.api.policy.Policy;
+
 import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatform;
 import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatformLauncherNoServer;
 import org.apache.brooklyn.core.util.ResourceUtils;
+
 import org.apache.brooklyn.entity.webapp.DynamicWebAppCluster;
 import org.apache.brooklyn.entity.webapp.JavaWebAppService;
 import org.apache.brooklyn.entity.webapp.WebAppService;
@@ -54,7 +56,7 @@ import brooklyn.entity.basic.Attributes;
 import brooklyn.entity.basic.BrooklynTaskTags;
 import brooklyn.entity.basic.Entities;
 import brooklyn.entity.basic.Lifecycle;
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
 import brooklyn.test.Asserts;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.exceptions.Exceptions;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogYamlCombiTest.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogYamlCombiTest.java b/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogYamlCombiTest.java
index 73bb59b..2baf194 100644
--- a/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogYamlCombiTest.java
+++ b/usage/camp/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogYamlCombiTest.java
@@ -31,7 +31,7 @@ import brooklyn.entity.basic.BasicEntity;
 import brooklyn.entity.basic.BasicStartable;
 import brooklyn.entity.basic.ConfigKeys;
 import brooklyn.entity.basic.Entities;
-import brooklyn.policy.ha.ServiceRestarter;
+import org.apache.brooklyn.policy.ha.ServiceRestarter;
 import brooklyn.util.exceptions.Exceptions;
 
 import com.google.common.collect.Iterables;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/camp/src/test/resources/java-web-app-and-db-with-policy.yaml
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/resources/java-web-app-and-db-with-policy.yaml b/usage/camp/src/test/resources/java-web-app-and-db-with-policy.yaml
index 671f64a..c2152b5 100644
--- a/usage/camp/src/test/resources/java-web-app-and-db-with-policy.yaml
+++ b/usage/camp/src/test/resources/java-web-app-and-db-with-policy.yaml
@@ -29,7 +29,7 @@ services:
       brooklyn.example.db.url: $brooklyn:formatString("jdbc:%s%s?user=%s&password=%s",
          component("db").attributeWhenReady("datastore.url"), "visitors", "brooklyn", "br00k11n")
   brooklyn.policies:
-  - policyType: brooklyn.policy.autoscaling.AutoScalerPolicy
+  - policyType: org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy
     brooklyn.config:
       metric: $brooklyn:sensor("org.apache.brooklyn.entity.webapp.DynamicWebAppCluster", "webapp.reqs.perSec.windowed.perNode")
       metricLowerBound: 10

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/camp/src/test/resources/vanilla-bash-netcat-w-client.yaml
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/resources/vanilla-bash-netcat-w-client.yaml b/usage/camp/src/test/resources/vanilla-bash-netcat-w-client.yaml
index d1bc4d4..3279dc7 100644
--- a/usage/camp/src/test/resources/vanilla-bash-netcat-w-client.yaml
+++ b/usage/camp/src/test/resources/vanilla-bash-netcat-w-client.yaml
@@ -34,13 +34,13 @@ services:
 
   # a failure detector and a service restarter work together
   brooklyn.enrichers:
-  - type: brooklyn.policy.ha.ServiceFailureDetector
+  - type: org.apache.brooklyn.policy.ha.ServiceFailureDetector
     brooklyn.config:
       # wait 15s after service fails before propagating failure
       serviceFailedStabilizationDelay: 15s
 
   brooklyn.policies:
-  - policyType: brooklyn.policy.ha.ServiceRestarter
+  - policyType: org.apache.brooklyn.policy.ha.ServiceRestarter
     brooklyn.config:
       # repeated failures in a time window can cause the restarter to abort,
       # propagating the failure; a time window of 0 will mean it always restarts!

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/cli/src/main/resources/brooklyn/default.catalog.bom
----------------------------------------------------------------------
diff --git a/usage/cli/src/main/resources/brooklyn/default.catalog.bom b/usage/cli/src/main/resources/brooklyn/default.catalog.bom
index 4cd5f6b..9255913 100644
--- a/usage/cli/src/main/resources/brooklyn/default.catalog.bom
+++ b/usage/cli/src/main/resources/brooklyn/default.catalog.bom
@@ -188,7 +188,7 @@ brooklyn.catalog:
         initialSize:    3
         # and add a policy to scale based on ops per minute
         brooklyn.policies:
-        - type: brooklyn.policy.autoscaling.AutoScalerPolicy
+        - type: org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy
           brooklyn.config:
             metric: riak.node.ops.1m.perNode
             # more than 100 ops per second (6k/min) scales out, less than 50 scales back
@@ -274,13 +274,13 @@ brooklyn.catalog:
                   enricher.window.duration: 30s
               
               # emit failure sensor if a failure connecting to the service is sustained for 30s
-              - type: brooklyn.policy.ha.ServiceFailureDetector
+              - type: org.apache.brooklyn.policy.ha.ServiceFailureDetector
                 brooklyn.config:
                   entityFailed.stabilizationDelay: 30s
             
               brooklyn.policies:
               # restart if a failure is detected (with a max of one restart in 2m, sensor will propagate otherwise) 
-              - type: brooklyn.policy.ha.ServiceRestarter
+              - type: org.apache.brooklyn.policy.ha.ServiceRestarter
                 brooklyn.config:
                   failOnRecurringFailuresInThisDuration: 2m
         
@@ -305,10 +305,10 @@ brooklyn.catalog:
         brooklyn.policies:
         # resilience: if a per-node restart policy fails,
         # just throw that node away and create a new one
-        - type: brooklyn.policy.ha.ServiceReplacer
+        - type: org.apache.brooklyn.policy.ha.ServiceReplacer
         
         # and scale based on reqs/sec
-        - type: brooklyn.policy.autoscaling.AutoScalerPolicy
+        - type: org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy
           brooklyn.config:
             # scale based on reqs/sec (though in a real-world situation, 
             # reqs.per_sec.windowed_30s.per_node might be a better choice) 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/launcher/src/test/resources/couchbase-w-loadgen.yaml
----------------------------------------------------------------------
diff --git a/usage/launcher/src/test/resources/couchbase-w-loadgen.yaml b/usage/launcher/src/test/resources/couchbase-w-loadgen.yaml
index 7dc4995..7803a8c 100644
--- a/usage/launcher/src/test/resources/couchbase-w-loadgen.yaml
+++ b/usage/launcher/src/test/resources/couchbase-w-loadgen.yaml
@@ -33,7 +33,7 @@ services:
       minRam: 16g
       minCores: 4
   brooklyn.policies:
-  - type: brooklyn.policy.autoscaling.AutoScalerPolicy
+  - type: org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy
     brooklyn.config:
       metric: $brooklyn:sensor("org.apache.brooklyn.entity.nosql.couchbase.CouchbaseCluster",
         "couchbase.stats.cluster.per.node.ops")

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java b/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
index bc7b2c3..2e70a2b 100644
--- a/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
+++ b/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
@@ -48,7 +48,7 @@ import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server;
 import org.apache.brooklyn.launcher.BrooklynLauncher;
 
 import org.apache.brooklyn.location.basic.PortRanges;
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
 import brooklyn.util.CommandLineUtil;
 import brooklyn.util.collections.MutableSet;
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/WebClusterApp.java
----------------------------------------------------------------------
diff --git a/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/WebClusterApp.java b/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/WebClusterApp.java
index 8b635f1..399f847 100644
--- a/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/WebClusterApp.java
+++ b/usage/qa/src/test/java/org/apache/brooklyn/qa/longevity/webcluster/WebClusterApp.java
@@ -35,7 +35,7 @@ import org.apache.brooklyn.entity.webapp.ControlledDynamicWebAppCluster;
 import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server;
 import org.apache.brooklyn.launcher.BrooklynLauncher;
 
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
 import brooklyn.util.CommandLineUtil;
 
 import com.google.common.collect.Lists;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ConfigSummary.java
----------------------------------------------------------------------
diff --git a/usage/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ConfigSummary.java b/usage/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ConfigSummary.java
index 5f79e4d..12d5a9d 100644
--- a/usage/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ConfigSummary.java
+++ b/usage/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ConfigSummary.java
@@ -86,7 +86,7 @@ public abstract class ConfigSummary implements HasName, Serializable {
         this.reconfigurable = config.isReconfigurable();
 
         /* Use String, to guarantee it is serializable; otherwise get:
-         *   No serializer found for class brooklyn.policy.autoscaling.AutoScalerPolicy$3 and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: java.util.ArrayList[9]->org.apache.brooklyn.rest.domain.PolicyConfigSummary["defaultValue"])
+         *   No serializer found for class org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy$3 and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: java.util.ArrayList[9]->org.apache.brooklyn.rest.domain.PolicyConfigSummary["defaultValue"])
          *   at org.codehaus.jackson.map.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:52)
          */
         this.label = label;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/CatalogResourceTest.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/CatalogResourceTest.java b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/CatalogResourceTest.java
index 83690d8..68c7082 100644
--- a/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/CatalogResourceTest.java
+++ b/usage/rest-server/src/test/java/org/apache/brooklyn/rest/resources/CatalogResourceTest.java
@@ -45,7 +45,7 @@ import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle;
 import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
 import org.apache.brooklyn.core.management.osgi.OsgiStandaloneTest;
 
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
 
 import org.apache.brooklyn.rest.domain.CatalogEntitySummary;
 import org.apache.brooklyn.rest.domain.CatalogItemSummary;


[24/24] incubator-brooklyn git commit: This closes #832

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


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

Branch: refs/heads/master
Commit: f092e183f9fc0ebc060d615a0efdea78357d9f87
Parents: bf2cfcd 3fff641
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Tue Aug 18 12:04:55 2015 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Tue Aug 18 12:04:55 2015 +0100

----------------------------------------------------------------------
 .../database/postgresql/PostgreSqlNode.java     | 23 ++++++-
 .../postgresql/PostgreSqlSshDriver.java         | 64 +++++++++++++++++---
 2 files changed, 77 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f092e183/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNode.java
----------------------------------------------------------------------
diff --cc software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNode.java
index 7d195f7,0000000..70ac0c7
mode 100644,000000..100644
--- a/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNode.java
+++ b/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlNode.java
@@@ -1,95 -1,0 +1,116 @@@
 +/*
 + * Licensed to the Apache Software Foundation (ASF) under one
 + * or more contributor license agreements.  See the NOTICE file
 + * distributed with this work for additional information
 + * regarding copyright ownership.  The ASF licenses this file
 + * to you under the Apache License, Version 2.0 (the
 + * "License"); you may not use this file except in compliance
 + * with the License.  You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing,
 + * software distributed under the License is distributed on an
 + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 + * KIND, either express or implied.  See the License for the
 + * specific language governing permissions and limitations
 + * under the License.
 + */
 +package org.apache.brooklyn.entity.database.postgresql;
 +
 +import org.apache.brooklyn.api.catalog.Catalog;
 +import org.apache.brooklyn.api.entity.Effector;
 +import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
 +import org.apache.brooklyn.api.entity.trait.HasShortName;
 +import org.apache.brooklyn.core.util.flags.SetFromFlag;
 +
 +import brooklyn.config.ConfigKey;
 +import brooklyn.entity.basic.ConfigKeys;
 +import brooklyn.entity.basic.SoftwareProcess;
 +import org.apache.brooklyn.entity.database.DatabaseNode;
 +import org.apache.brooklyn.entity.database.DatastoreMixins;
 +import org.apache.brooklyn.entity.database.DatastoreMixins.DatastoreCommon;
 +import brooklyn.entity.effector.Effectors;
++import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
 +import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
- 
 +import org.apache.brooklyn.location.basic.PortRanges;
 +
 +/**
 + * PostgreSQL database node entity.
 + * <p>
 + * <ul>
 + * <li>You may need to increase shared memory settings in the kernel depending on the setting of
 + * the {@link #SHARED_MEMORY_BUFFER} key. The minimumm value is <em>128kB</em>. See the PostgreSQL
 + * <a href="http://www.postgresql.org/docs/9.1/static/kernel-resources.html">documentation</a>.
 + * <li>You will also need to enable passwordless sudo.
 + * </ul>
 + */
 +@Catalog(name="PostgreSQL Node", description="PostgreSQL is an object-relational database management system (ORDBMS)", iconUrl="classpath:///postgresql-logo-200px.png")
 +@ImplementedBy(PostgreSqlNodeImpl.class)
 +public interface PostgreSqlNode extends SoftwareProcess, HasShortName, DatastoreCommon, DatabaseNode {
 +    
 +    @SetFromFlag("version")
 +    ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "9.3-1");//"9.1-4");
 +
 +    @SetFromFlag("configFileUrl")
 +    ConfigKey<String> CONFIGURATION_FILE_URL = ConfigKeys.newStringConfigKey(
 +            "postgresql.config.file.url", "URL where PostgreSQL configuration file can be found; "
 +                + "if not supplied the blueprint uses the default and customises it");
 +
 +    @SetFromFlag("authConfigFileUrl")
 +    ConfigKey<String> AUTHENTICATION_CONFIGURATION_FILE_URL = ConfigKeys.newStringConfigKey(
 +            "postgresql.authConfig.file.url", "URL where PostgreSQL host-based authentication configuration file can be found; "
 +                + "if not supplied the blueprint uses the default and customises it");
 +
 +    @SetFromFlag("port")
 +    PortAttributeSensorAndConfigKey POSTGRESQL_PORT = new PortAttributeSensorAndConfigKey(
 +            "postgresql.port", "PostgreSQL port", PortRanges.fromString("5432+"));
 +
 +    @SetFromFlag("sharedMemory")
 +    ConfigKey<String> SHARED_MEMORY = ConfigKeys.newStringConfigKey(
 +            "postgresql.sharedMemory", "Size of shared memory buffer (must specify as kB, MB or GB, minimum 128kB)", "4MB");
 +
 +    @SetFromFlag("maxConnections")
 +    ConfigKey<Integer> MAX_CONNECTIONS = ConfigKeys.newIntegerConfigKey(
 +            "postgresql.maxConnections", "Maximum number of connections to the database", 100);
 +
 +    @SetFromFlag("disconnectOnStop")
 +    ConfigKey<Boolean> DISCONNECT_ON_STOP = ConfigKeys.newBooleanConfigKey(
 +            "postgresql.disconnect.on.stop", "If true, PostgreSQL will immediately disconnet (pg_ctl -m immediate stop) all current connections when the node is stopped", true);
 +
 +    @SetFromFlag("pollPeriod")
 +    ConfigKey<Long> POLL_PERIOD = ConfigKeys.newLongConfigKey(
 +            "postgresql.sensorpoll", "Poll period (in milliseconds)", 1000L);
++    
++    @SetFromFlag("initializeDB")
++    ConfigKey<Boolean> INITIALIZE_DB = ConfigKeys.newBooleanConfigKey(
++            "postgresql.initialize", "If true, PostgreSQL will create a new user and database", false);
++
++    @SetFromFlag("username")
++    BasicAttributeSensorAndConfigKey<String> USERNAME = new BasicAttributeSensorAndConfigKey<>(
++            String.class, "postgresql.username", "Username of the database user");
++    
++    String DEFAULT_USERNAME = "postgresqluser";
++    
++    @SetFromFlag("password")
++    BasicAttributeSensorAndConfigKey<String> PASSWORD = new BasicAttributeSensorAndConfigKey<>(
++            String.class, "postgresql.password",
++            "Password for the database user, auto-generated if not set");
++
++    @SetFromFlag("database")
++    BasicAttributeSensorAndConfigKey<String> DATABASE = new BasicAttributeSensorAndConfigKey<>(
++            String.class, "postgresql.database", "Database to be used");
++    
++    String DEFAULT_DB_NAME = "db";
 +
 +    Effector<String> EXECUTE_SCRIPT = Effectors.effector(DatastoreMixins.EXECUTE_SCRIPT)
 +            .description("Executes the given script contents using psql")
 +            .buildAbstract();
 +
 +    Integer getPostgreSqlPort();
 +    String getSharedMemory();
 +    Integer getMaxConnections();
 +
 +    String executeScript(String commands);
 +
 +}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f092e183/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
----------------------------------------------------------------------
diff --cc software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
index d66ed76,0000000..54b88a2
mode 100644,000000..100644
--- a/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
+++ b/software/database/src/main/java/org/apache/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
@@@ -1,425 -1,0 +1,471 @@@
 +/*
 + * Licensed to the Apache Software Foundation (ASF) under one
 + * or more contributor license agreements.  See the NOTICE file
 + * distributed with this work for additional information
 + * regarding copyright ownership.  The ASF licenses this file
 + * to you under the Apache License, Version 2.0 (the
 + * "License"); you may not use this file except in compliance
 + * with the License.  You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing,
 + * software distributed under the License is distributed on an
 + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 + * KIND, either express or implied.  See the License for the
 + * specific language governing permissions and limitations
 + * under the License.
 + */
 +package org.apache.brooklyn.entity.database.postgresql;
 +
 +import static brooklyn.util.ssh.BashCommands.INSTALL_WGET;
 +import static brooklyn.util.ssh.BashCommands.alternativesGroup;
 +import static brooklyn.util.ssh.BashCommands.chainGroup;
 +import static brooklyn.util.ssh.BashCommands.dontRequireTtyForSudo;
 +import static brooklyn.util.ssh.BashCommands.executeCommandThenAsUserTeeOutputToFile;
 +import static brooklyn.util.ssh.BashCommands.fail;
 +import static brooklyn.util.ssh.BashCommands.ifExecutableElse0;
 +import static brooklyn.util.ssh.BashCommands.ifExecutableElse1;
 +import static brooklyn.util.ssh.BashCommands.installPackage;
 +import static brooklyn.util.ssh.BashCommands.sudo;
 +import static brooklyn.util.ssh.BashCommands.sudoAsUser;
 +import static brooklyn.util.ssh.BashCommands.warn;
 +import static java.lang.String.format;
 +
 +import java.io.File;
 +import java.io.IOException;
 +import java.io.InputStream;
 +
 +import javax.annotation.Nullable;
 +
++import org.apache.brooklyn.api.location.OsDetails;
++import org.apache.brooklyn.core.util.task.DynamicTasks;
++import org.apache.brooklyn.core.util.task.ssh.SshTasks;
++import org.apache.brooklyn.core.util.task.ssh.SshTasks.OnFailingTask;
++import org.apache.brooklyn.core.util.task.system.ProcessTaskWrapper;
++import org.apache.brooklyn.location.basic.SshMachineLocation;
 +import org.slf4j.Logger;
 +import org.slf4j.LoggerFactory;
 +
 +import brooklyn.entity.basic.AbstractSoftwareProcessSshDriver;
 +import brooklyn.entity.basic.Attributes;
 +import brooklyn.entity.basic.SoftwareProcess;
 +import org.apache.brooklyn.entity.database.DatastoreMixins;
 +import brooklyn.entity.software.SshEffectorTasks;
- 
- import org.apache.brooklyn.api.location.OsDetails;
- import org.apache.brooklyn.core.util.task.DynamicTasks;
- import org.apache.brooklyn.core.util.task.ssh.SshTasks;
- import org.apache.brooklyn.core.util.task.ssh.SshTasks.OnFailingTask;
- import org.apache.brooklyn.core.util.task.system.ProcessTaskWrapper;
- import org.apache.brooklyn.location.basic.SshMachineLocation;
- 
++import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
 +import brooklyn.util.collections.MutableList;
 +import brooklyn.util.collections.MutableMap;
 +import brooklyn.util.exceptions.Exceptions;
 +import brooklyn.util.net.Urls;
 +import brooklyn.util.os.Os;
 +import brooklyn.util.stream.Streams;
 +import brooklyn.util.text.Identifiers;
++import brooklyn.util.text.StringEscapes;
 +import brooklyn.util.text.StringFunctions;
 +import brooklyn.util.text.Strings;
 +
 +import com.google.common.base.Charsets;
 +import com.google.common.base.Function;
 +import com.google.common.collect.ImmutableList;
 +import com.google.common.collect.ImmutableMap;
 +import com.google.common.collect.Iterables;
 +import com.google.common.io.Files;
 +
 +/**
 + * The SSH implementation of the {@link PostgreSqlDriver}.
 + */
 +public class PostgreSqlSshDriver extends AbstractSoftwareProcessSshDriver implements PostgreSqlDriver {
 +
 +    public static final Logger log = LoggerFactory.getLogger(PostgreSqlSshDriver.class);
 +
 +    public PostgreSqlSshDriver(PostgreSqlNodeImpl entity, SshMachineLocation machine) {
 +        super(entity, machine);
 +
 +        entity.setAttribute(Attributes.LOG_FILE_LOCATION, getLogFile());
 +    }
 +
 +    /*
 +     * TODO this is much messier than we would like because postgres runs as user postgres,
 +     * meaning the dirs must be RW by that user, and accessible (thus all parent paths),
 +     * which may rule out putting it in a location used by the default user.
 +     * Two irritating things:
 +     * * currently we sometimes make up a different onbox base dir;
 +     * * currently we put files to /tmp for staging
 +     * Could investigate if it really needs to run as user postgres;
 +     * could also see whether default user can be added to group postgres,
 +     * and the run dir (and all parents) made accessible to group postgres.
 +     */
 +    @Override
 +    public void install() {
 +        String version = getEntity().getConfig(SoftwareProcess.SUGGESTED_VERSION);
 +        String majorMinorVersion = version.substring(0, version.lastIndexOf("-"));
 +        String shortVersion = majorMinorVersion.replace(".", "");
 +
 +        String altTarget = "/opt/brooklyn/postgres/";
 +        String altInstallDir = Urls.mergePaths(altTarget, "install/"+majorMinorVersion);
 +        
 +        Iterable<String> pgctlLocations = ImmutableList.of(
 +            altInstallDir+"/bin",
 +            "/usr/lib/postgresql/"+majorMinorVersion+"/bin/",
 +            "/opt/local/lib/postgresql"+shortVersion+"/bin/",
 +            "/usr/pgsql-"+majorMinorVersion+"/bin",
 +            "/usr/local/bin/",
 +            "/usr/bin/",
 +            "/bin/");
 +
 +        DynamicTasks.queueIfPossible(SshTasks.dontRequireTtyForSudo(getMachine(),
 +            // sudo is absolutely required here, in customize we set user to postgres
 +            OnFailingTask.FAIL)).orSubmitAndBlock();
 +        DynamicTasks.waitForLast();
 +
 +        // Check whether we can find a usable pg_ctl, and if not install one
 +        MutableList<String> findOrInstall = MutableList.<String>of()
 +            .append("which pg_ctl")
 +            .appendAll(Iterables.transform(pgctlLocations, StringFunctions.formatter("test -x %s/pg_ctl")))
 +            .append(installPackage(ImmutableMap.of(
 +                "yum", "postgresql"+shortVersion+" postgresql"+shortVersion+"-server",
 +                "apt", "postgresql-"+majorMinorVersion,
 +                "port", "postgresql"+shortVersion+" postgresql"+shortVersion+"-server"
 +                ), null))
 +                // due to impl of installPackage, it will not come to the line below I don't think
 +                .append(warn(format("WARNING: failed to find or install postgresql %s binaries", majorMinorVersion)));
 +
 +        // Link to correct binaries folder (different versions of pg_ctl and psql don't always play well together)
 +        MutableList<String> linkFromHere = MutableList.<String>of()
 +            .append(ifExecutableElse1("pg_ctl", chainGroup(
 +                "PG_EXECUTABLE=`which pg_ctl`",
 +                "PG_DIR=`dirname $PG_EXECUTABLE`",
 +                "echo 'found pg_ctl in '$PG_DIR' on path so linking PG bin/ to that dir'",
 +                "ln -s $PG_DIR bin")))
 +                .appendAll(Iterables.transform(pgctlLocations, givenDirIfFileExistsInItLinkToDir("pg_ctl", "bin")))
 +                .append(fail(format("WARNING: failed to find postgresql %s binaries for pg_ctl, may already have another version installed; aborting", majorMinorVersion), 9));
 +
 +        newScript(INSTALLING)
 +        .body.append(
 +            dontRequireTtyForSudo(),
 +            ifExecutableElse0("yum", getYumRepository(version, majorMinorVersion, shortVersion)),
 +            ifExecutableElse0("apt-get", getAptRepository()),
 +            "rm -f bin", // if left over from previous incomplete/failed install (not sure why that keeps happening!)
 +            alternativesGroup(findOrInstall),
 +            alternativesGroup(linkFromHere))
 +            .failOnNonZeroResultCode()
 +            .queue();
 +        
 +        // check that the proposed install dir is one that user postgres can access
 +        if (DynamicTasks.queue(SshEffectorTasks.ssh(sudoAsUser("postgres", "ls "+getInstallDir())).allowingNonZeroExitCode()
 +                .summary("check postgres user can access install dir")).asTask().getUnchecked()!=0) {
 +            log.info("Postgres install dir "+getInstallDir()+" for "+getEntity()+" is not accessible to user 'postgres'; " + "using "+altInstallDir+" instead");
 +            String newRunDir = Urls.mergePaths(altTarget, "apps", getEntity().getApplication().getId(), getEntity().getId());
 +            if (DynamicTasks.queue(SshEffectorTasks.ssh("ls "+altInstallDir+"/pg_ctl").allowingNonZeroExitCode()
 +                    .summary("check whether "+altInstallDir+" is set up")).asTask().getUnchecked()==0) {
 +                // alt target already exists with binary; nothing to do for install
 +            } else {
 +                DynamicTasks.queue(SshEffectorTasks.ssh(
 +                    "mkdir -p "+altInstallDir,
 +                    "rm -rf '"+altInstallDir+"'",
 +                    "mv "+getInstallDir()+" "+altInstallDir,
 +                    "rm -rf '"+getInstallDir()+"'",
 +                    "ln -s "+altInstallDir+" "+getInstallDir(),
 +                    "mkdir -p " + newRunDir,
 +                    "chown -R postgres:postgres "+altTarget).runAsRoot().requiringExitCodeZero()
 +                    .summary("move install dir from user to postgres owned space"));
 +            }
 +            DynamicTasks.waitForLast();
 +            setInstallDir(altInstallDir);
 +            setRunDir(newRunDir);
 +        }
 +    }
 +
 +    private String getYumRepository(String version, String majorMinorVersion, String shortVersion) {
 +        // postgres becomes available if you add the repos using an RPM such as
 +        // http://yum.postgresql.org/9.3/redhat/rhel-6-i386/pgdg-centos93-9.3-1.noarch.rpm
 +        // fedora, rhel, sl, and centos supported for RPM's
 +
 +        OsDetails osDetails = getMachine().getMachineDetails().getOsDetails();
 +        String arch = osDetails.getArch();
 +        String osMajorVersion = osDetails.getVersion();
 +        String osName = osDetails.getName();
 +
 +        log.debug("postgres detecting yum information for "+getEntity()+" at "+getMachine()+": "+osName+", "+osMajorVersion+", "+arch);
 +
 +        if (osName==null) osName = ""; else osName = osName.toLowerCase();
 +
 +        if (osName.equals("ubuntu")) return "echo skipping yum repo setup as this is not an rpm environment";
 +
 +        if (osName.equals("rhel")) osName = "redhat";
 +        else if (osName.equals("centos")) osName = "centos";
 +        else if (osName.equals("sl") || osName.startsWith("scientific")) osName = "sl";
 +        else if (osName.equals("fedora")) osName = "fedora";
 +        else {
 +            log.debug("insufficient OS family information '"+osName+"' for "+getMachine()+" when installing "+getEntity()+" (yum repos); treating as centos");
 +            osName = "centos";
 +        }
 +
 +        if (Strings.isBlank(arch)) {
 +            log.warn("Insuffient architecture information '"+arch+"' for "+getMachine()+"when installing "+getEntity()+"; treating as x86_64");
 +            arch = "x86_64";
 +        }
 +
 +        if (Strings.isBlank(osMajorVersion)) {
 +            if (osName.equals("fedora")) osMajorVersion = "20";
 +            else osMajorVersion = "6";
 +            log.warn("Insuffient OS version information '"+getMachine().getOsDetails().getVersion()+"' for "+getMachine()+"when installing "+getEntity()+" (yum repos); treating as "+osMajorVersion);
 +        } else {
 +            if (osMajorVersion.indexOf(".")>0) 
 +                osMajorVersion = osMajorVersion.substring(0, osMajorVersion.indexOf('.'));
 +        }
 +
 +        return chainGroup(
 +                INSTALL_WGET,
 +                sudo(format("wget http://yum.postgresql.org/%s/redhat/rhel-%s-%s/pgdg-%s%s-%s.noarch.rpm", majorMinorVersion, osMajorVersion, arch, osName, shortVersion, version)),
 +                sudo(format("rpm -Uvh pgdg-%s%s-%s.noarch.rpm", osName, shortVersion, version))
 +            );
 +    }
 +
 +    private String getAptRepository() {
 +        return chainGroup(
 +                INSTALL_WGET,
 +                "wget --quiet -O - http://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | sudo tee -a apt-key add -",
 +                "echo \"deb http://apt.postgresql.org/pub/repos/apt/   $(sudo lsb_release --codename --short)-pgdg main\" | sudo tee -a /etc/apt/sources.list.d/postgresql.list"
 +            );
 +    }
 +
 +    private static Function<String, String> givenDirIfFileExistsInItLinkToDir(final String filename, final String linkToMake) {
 +        return new Function<String, String>() {
 +            public String apply(@Nullable String dir) {
 +                return ifExecutableElse1(Urls.mergePaths(dir, filename),
 +                    chainGroup("echo 'found "+filename+" in "+dir+" so linking to it in "+linkToMake+"'", "ln -s "+dir+" "+linkToMake));
 +            }
 +        };
 +    }
 +
 +    @Override
 +    public void customize() {
 +        // Some OSes start postgres during package installation
 +        DynamicTasks.queue(SshEffectorTasks.ssh(sudoAsUser("postgres", "/etc/init.d/postgresql stop")).allowingNonZeroExitCode()).get();
 +
 +        newScript(CUSTOMIZING)
 +        .body.append(
 +            sudo("mkdir -p " + getDataDir()),
 +            sudo("chown postgres:postgres " + getDataDir()),
 +            sudo("chmod 700 " + getDataDir()),
 +            sudo("touch " + getLogFile()),
 +            sudo("chown postgres:postgres " + getLogFile()),
 +            sudo("touch " + getPidFile()),
 +            sudo("chown postgres:postgres " + getPidFile()),
 +            alternativesGroup(
 +                chainGroup(format("test -e %s", getInstallDir() + "/bin/initdb"),
 +                    sudoAsUser("postgres", getInstallDir() + "/bin/initdb -D " + getDataDir())),
 +                    callPgctl("initdb", true)))
 +                    .failOnNonZeroResultCode()
 +                    .execute();
 +
 +        String configUrl = getEntity().getConfig(PostgreSqlNode.CONFIGURATION_FILE_URL);
 +        if (Strings.isBlank(configUrl)) {
 +            // http://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server
 +            // If the same setting is listed multiple times, the last one wins.
 +            DynamicTasks.queue(SshEffectorTasks.ssh(
 +                executeCommandThenAsUserTeeOutputToFile(
 +                    chainGroup(
 +                        "echo \"listen_addresses = '*'\"",
 +                        "echo \"port = " + getEntity().getPostgreSqlPort() +  "\"",
 +                        "echo \"max_connections = " + getEntity().getMaxConnections() +  "\"",
 +                        "echo \"shared_buffers = " + getEntity().getSharedMemory() +  "\"",
 +                        "echo \"external_pid_file = '" + getPidFile() +  "'\""),
 +                        "postgres", getDataDir() + "/postgresql.conf")));
 +        } else {
 +            String contents = processTemplate(configUrl);
 +            DynamicTasks.queue(
 +                SshEffectorTasks.put("/tmp/postgresql.conf").contents(contents),
 +                SshEffectorTasks.ssh(sudoAsUser("postgres", "cp /tmp/postgresql.conf " + getDataDir() + "/postgresql.conf")));
 +        }
 +
 +        String authConfigUrl = getEntity().getConfig(PostgreSqlNode.AUTHENTICATION_CONFIGURATION_FILE_URL);
 +        if (Strings.isBlank(authConfigUrl)) {
 +            DynamicTasks.queue(SshEffectorTasks.ssh(
 +                // TODO give users control which hosts can connect and the authentication mechanism
 +                executeCommandThenAsUserTeeOutputToFile("echo \"host all all 0.0.0.0/0 md5\"", "postgres", getDataDir() + "/pg_hba.conf")));
 +        } else {
 +            String contents = processTemplate(authConfigUrl);
 +            DynamicTasks.queue(
 +                SshEffectorTasks.put("/tmp/pg_hba.conf").contents(contents),
 +                SshEffectorTasks.ssh(sudoAsUser("postgres", "cp /tmp/pg_hba.conf " + getDataDir() + "/pg_hba.conf")));
 +        }
 +
 +        // Wait for commands to complete before running the creation script
 +        DynamicTasks.waitForLast();
- 
++        if(entity.getConfig(PostgreSqlNode.INITIALIZE_DB)){
++            initializeNewDatabase();
++        }
 +        // Capture log file contents if there is an error configuring the database
 +        try {
 +            executeDatabaseCreationScript();
 +        } catch (RuntimeException r) {
 +            logTailOfPostgresLog();
 +            throw Exceptions.propagate(r);
 +        }
 +
 +        // Try establishing an external connection. If you get a "Connection refused...accepting TCP/IP connections
 +        // on port 5432?" error then the port is probably closed. Check that the firewall allows external TCP/IP
 +        // connections (netstat -nap). You can open a port with lokkit or by configuring the iptables.
 +    }
 +
++    private void initializeNewDatabase() {
++        String createUserCommand = String.format(
++                "\"CREATE USER %s WITH PASSWORD '%s'; \"",
++                StringEscapes.escapeSql(getUsername()), 
++                StringEscapes.escapeSql(getUserPassword())
++        );
++        String createDatabaseCommand = String.format(
++                "\"CREATE DATABASE %s OWNER %s\"",
++                StringEscapes.escapeSql(getDatabaseName()),
++                StringEscapes.escapeSql(getUsername()));
++        newScript("initializing user and database")
++        .body.append(
++                "cd " + getInstallDir(),
++                callPgctl("start", true),
++                sudoAsUser("postgres", getInstallDir() + "/bin/psql -p " + entity.getAttribute(PostgreSqlNode.POSTGRESQL_PORT) + 
++                        " --command="+ createUserCommand),
++                sudoAsUser("postgres", getInstallDir() + "/bin/psql -p " + entity.getAttribute(PostgreSqlNode.POSTGRESQL_PORT) + 
++                                " --command="+ createDatabaseCommand),
++                callPgctl("stop", true))
++                .failOnNonZeroResultCode().execute();
++    }
++    
++    private String getConfigOrDefault(BasicAttributeSensorAndConfigKey<String> key, String def) {
++        String config = entity.getConfig(key);
++        if(Strings.isEmpty(config)) {
++            config = def;
++            log.debug(entity + " has no config specified for " + key + "; using default `" + def + "`");
++            entity.setAttribute(key, config);
++        }
++        return config;
++    }
++    
++    protected String getDatabaseName() {
++        return getConfigOrDefault(PostgreSqlNode.DATABASE, PostgreSqlNode.DEFAULT_DB_NAME);
++    }
++    
++    protected String getUsername(){
++        return getConfigOrDefault(PostgreSqlNode.USERNAME, PostgreSqlNode.DEFAULT_USERNAME);
++    }
++    
++    protected String getUserPassword() {
++        return getConfigOrDefault(PostgreSqlNode.PASSWORD, Strings.makeRandomId(8));
++    }
++
 +    protected void executeDatabaseCreationScript() {
 +        if (copyDatabaseCreationScript()) {
 +            newScript("running postgres creation script")
 +            .body.append(
 +                "cd " + getInstallDir(),
 +                callPgctl("start", true),
 +                sudoAsUser("postgres", getInstallDir() + "/bin/psql -p " + entity.getAttribute(PostgreSqlNode.POSTGRESQL_PORT) + " --file " + getRunDir() + "/creation-script.sql"),
 +                callPgctl("stop", true))
 +                .failOnNonZeroResultCode()
 +                .execute();
 +        }
 +    }
 +
 +    private boolean installFile(InputStream contents, String destName) {
 +        String uid = Identifiers.makeRandomId(8);
 +        // TODO currently put in /tmp for staging, since run dir may not be accessible to ssh user
 +        getMachine().copyTo(contents, "/tmp/"+destName+"_"+uid);
 +        DynamicTasks.queueIfPossible(SshEffectorTasks.ssh(
 +            "cd "+getRunDir(), 
 +            "mv /tmp/"+destName+"_"+uid+" "+destName,
 +            "chown postgres:postgres "+destName,
 +            "chmod 644 "+destName)
 +            .runAsRoot().requiringExitCodeZero())
 +            .orSubmitAndBlock(getEntity()).andWaitForSuccess();
 +        return true;
 +    }
 +    private boolean copyDatabaseCreationScript() {
 +        InputStream creationScript = DatastoreMixins.getDatabaseCreationScript(entity);
 +        if (creationScript==null)
 +            return false;
 +        return installFile(creationScript, "creation-script.sql");
 +    }
 +
 +    public String getDataDir() {
 +        return getRunDir() + "/data";
 +    }
 +
 +    public String getLogFile() {
 +        return getRunDir() + "/postgresql.log";
 +    }
 +
 +    public String getPidFile() {
 +        return getRunDir() + "/postgresql.pid";
 +    }
 +
 +    /** @deprecated since 0.7.0 renamed {@link #logTailOfPostgresLog()} */
 +    @Deprecated
 +    public void copyLogFileContents() { logTailOfPostgresLog(); }
 +    public void logTailOfPostgresLog() {
 +        try {
 +            File file = Os.newTempFile("postgresql-"+getEntity().getId(), "log");
 +            int result = getMachine().copyFrom(getLogFile(), file.getAbsolutePath());
 +            if (result != 0) throw new IllegalStateException("Could not access log file " + getLogFile());
 +            log.info("Saving {} contents as {}", getLogFile(), file);
 +            Streams.logStreamTail(log, "postgresql.log", Streams.byteArrayOfString(Files.toString(file, Charsets.UTF_8)), 1024);
 +            file.delete();
 +        } catch (IOException ioe) {
 +            log.debug("Error reading copied log file: {}", ioe);
 +        }
 +    }
 +
 +    protected String callPgctl(String command, boolean waitForIt) {
 +        return sudoAsUser("postgres", getInstallDir() + "/bin/pg_ctl -D " + getDataDir() +
 +            " -l " + getLogFile() + (waitForIt ? " -w " : " ") + command);
 +    }
 +
 +    @Override
 +    public void launch() {
 +        log.info(String.format("Starting entity %s at %s", this, getLocation()));
 +        newScript(MutableMap.of("usePidFile", false), LAUNCHING)
 +        .body.append(callPgctl("start", false))
 +        .execute();
 +    }
 +
 +    @Override
 +    public boolean isRunning() {
 +        return newScript(MutableMap.of("usePidFile", getPidFile()), CHECK_RUNNING)
 +            .body.append(getStatusCmd())
 +            .execute() == 0;
 +    }
 +
 +    @Override
 +    public void stop() {
 +        newScript(MutableMap.of("usePidFile", false), STOPPING)
 +        .body.append(callPgctl((entity.getConfig(PostgreSqlNode.DISCONNECT_ON_STOP) ? "-m immediate " : "") + "stop", false))
 +        .failOnNonZeroResultCode()
 +        .execute();
 +        newScript(MutableMap.of("usePidFile", getPidFile(), "processOwner", "postgres"), STOPPING).execute();
 +    }
 +
 +    @Override
 +    public PostgreSqlNodeImpl getEntity() {
 +        return (PostgreSqlNodeImpl) super.getEntity();
 +    }
 +
 +    @Override
 +    public String getStatusCmd() {
 +        return callPgctl("status", false);
 +    }
 +
 +    public ProcessTaskWrapper<Integer> executeScriptAsync(String commands) {
 +        String filename = "postgresql-commands-"+Identifiers.makeRandomId(8);
 +        installFile(Streams.newInputStreamWithContents(commands), filename);
 +        return executeScriptFromInstalledFileAsync(filename);
 +    }
 +
 +    public ProcessTaskWrapper<Integer> executeScriptFromInstalledFileAsync(String filenameAlreadyInstalledAtServer) {
 +        return DynamicTasks.queue(
 +            SshEffectorTasks.ssh(
 +                "cd "+getRunDir(),
 +                sudoAsUser("postgres", getInstallDir() + "/bin/psql -p " + entity.getAttribute(PostgreSqlNode.POSTGRESQL_PORT) + " --file " + filenameAlreadyInstalledAtServer))
 +                .summary("executing datastore script "+filenameAlreadyInstalledAtServer));
 +    }
 +
 +}


[07/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/ha/ConnectionFailureDetectorTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/ha/ConnectionFailureDetectorTest.java b/policy/src/test/java/org/apache/brooklyn/policy/ha/ConnectionFailureDetectorTest.java
new file mode 100644
index 0000000..e2d6998
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/ha/ConnectionFailureDetectorTest.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.apache.brooklyn.api.policy.PolicySpec;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Entities;
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.net.HostAndPort;
+
+public class ConnectionFailureDetectorTest {
+
+    private static final int TIMEOUT_MS = 30*1000;
+    private static final int OVERHEAD = 250;
+    private static final int POLL_PERIOD = 100;
+
+    private ManagementContext managementContext;
+    private TestApplication app;
+    
+    private List<SensorEvent<FailureDescriptor>> events;
+    
+    private ServerSocket serverSocket;
+    private HostAndPort serverSocketAddress;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        events = new CopyOnWriteArrayList<SensorEvent<FailureDescriptor>>();
+        
+        managementContext = new LocalManagementContextForTests();
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+        
+        app.getManagementContext().getSubscriptionManager().subscribe(
+                app, 
+                HASensors.CONNECTION_FAILED, 
+                new SensorEventListener<FailureDescriptor>() {
+                    @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
+                        events.add(event);
+                    }
+                });
+        app.getManagementContext().getSubscriptionManager().subscribe(
+                app, 
+                HASensors.CONNECTION_RECOVERED, 
+                new SensorEventListener<FailureDescriptor>() {
+                    @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
+                        events.add(event);
+                    }
+                });
+        
+        serverSocketAddress = startServerSocket();
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        stopServerSocket();
+        if (managementContext != null) Entities.destroyAll(managementContext);
+    }
+    
+    private HostAndPort startServerSocket() throws Exception {
+        if (serverSocketAddress != null) {
+            serverSocket = new ServerSocket(serverSocketAddress.getPort());
+        } else {
+            for (int i = 40000; i < 40100; i++) {
+                try {
+                    serverSocket = new ServerSocket(i);
+                } catch (IOException e) {
+                    // try next port
+                }
+            }
+            assertNotNull(serverSocket, "Failed to create server socket; no ports free in range!");
+            serverSocketAddress = HostAndPort.fromParts(serverSocket.getInetAddress().getHostAddress(), serverSocket.getLocalPort());
+        }
+        return serverSocketAddress;
+    }
+
+    private void stopServerSocket() throws Exception {
+        if (serverSocket != null) serverSocket.close();
+    }
+
+    @Test(groups="Integration") // Has a 1 second wait
+    public void testNotNotifiedOfFailuresForHealthy() throws Exception {
+        // Create members before and after the policy is registered, to test both scenarios
+        
+        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
+                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress));
+        
+        assertNoEventsContinually();
+    }
+    
+    @Test
+    public void testNotifiedOfFailure() throws Exception {
+        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
+                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress));
+
+        stopServerSocket();
+
+        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
+        assertEquals(events.size(), 1, "events="+events);
+    }
+    
+    @Test
+    public void testNotifiedOfRecovery() throws Exception {
+        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
+                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress));
+        
+        stopServerSocket();
+        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
+
+        // make the connection recover
+        startServerSocket();
+        assertHasEventEventually(HASensors.CONNECTION_RECOVERED, Predicates.<Object>equalTo(app), null);
+        assertEquals(events.size(), 2, "events="+events);
+    }
+    
+    @Test
+    public void testReportsFailureWhenAlreadyDownOnRegisteringPolicy() throws Exception {
+        stopServerSocket();
+
+        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
+                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress));
+
+        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
+    }
+
+    @Test(groups="Integration") // Because slow
+    public void testNotNotifiedOfTemporaryFailuresDuringStabilisationDelay() throws Exception {
+        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
+                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
+                .configure(ConnectionFailureDetector.CONNECTION_FAILED_STABILIZATION_DELAY, Duration.ONE_MINUTE));
+        
+        stopServerSocket();
+        Thread.sleep(100);
+        startServerSocket();
+
+        assertNoEventsContinually();
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testNotifiedOfFailureAfterStabilisationDelay() throws Exception {
+        final int stabilisationDelay = 1000;
+        
+        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
+                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
+                .configure(ConnectionFailureDetector.CONNECTION_FAILED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
+        
+        stopServerSocket();
+
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testFailuresThenUpDownResetsStabilisationCount() throws Exception {
+        final long stabilisationDelay = 1000;
+        
+        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
+                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
+                .configure(ConnectionFailureDetector.CONNECTION_FAILED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
+        
+        stopServerSocket();
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+
+        startServerSocket();
+        Thread.sleep(POLL_PERIOD+OVERHEAD);
+        stopServerSocket();
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+        
+        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testNotNotifiedOfTemporaryRecoveryDuringStabilisationDelay() throws Exception {
+        final long stabilisationDelay = 1000;
+        
+        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
+                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
+                .configure(ConnectionFailureDetector.CONNECTION_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
+        
+        stopServerSocket();
+        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
+        events.clear();
+        
+        startServerSocket();
+        Thread.sleep(POLL_PERIOD+OVERHEAD);
+        stopServerSocket();
+
+        assertNoEventsContinually(Duration.of(stabilisationDelay + OVERHEAD));
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testNotifiedOfRecoveryAfterStabilisationDelay() throws Exception {
+        final int stabilisationDelay = 1000;
+        
+        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
+                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
+                .configure(ConnectionFailureDetector.CONNECTION_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
+        
+        stopServerSocket();
+        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
+        events.clear();
+
+        startServerSocket();
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+        assertHasEventEventually(HASensors.CONNECTION_RECOVERED, Predicates.<Object>equalTo(app), null);
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testRecoversThenDownUpResetsStabilisationCount() throws Exception {
+        final long stabilisationDelay = 1000;
+        
+        app.addPolicy(PolicySpec.create(ConnectionFailureDetector.class)
+                .configure(ConnectionFailureDetector.ENDPOINT, serverSocketAddress)
+                .configure(ConnectionFailureDetector.CONNECTION_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
+        
+        stopServerSocket();
+        assertHasEventEventually(HASensors.CONNECTION_FAILED, Predicates.<Object>equalTo(app), null);
+        events.clear();
+        
+        startServerSocket();
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+        
+        stopServerSocket();
+        Thread.sleep(POLL_PERIOD+OVERHEAD);
+        startServerSocket();
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+
+        assertHasEventEventually(HASensors.CONNECTION_RECOVERED, Predicates.<Object>equalTo(app), null);
+    }
+
+    private void assertHasEvent(Sensor<?> sensor, Predicate<Object> componentPredicate, Predicate<? super CharSequence> descriptionPredicate) {
+        for (SensorEvent<FailureDescriptor> event : events) {
+            if (event.getSensor().equals(sensor) && 
+                    (componentPredicate == null || componentPredicate.apply(event.getValue().getComponent())) &&
+                    (descriptionPredicate == null || descriptionPredicate.apply(event.getValue().getDescription()))) {
+                return;
+            }
+        }
+        fail("No matching "+sensor+" event found; events="+events);
+    }
+    
+    private void assertHasEventEventually(final Sensor<?> sensor, final Predicate<Object> componentPredicate, final Predicate<? super CharSequence> descriptionPredicate) {
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            @Override public void run() {
+                assertHasEvent(sensor, componentPredicate, descriptionPredicate);
+            }});
+    }
+    
+    private void assertNoEventsContinually(Duration duration) {
+        Asserts.succeedsContinually(ImmutableMap.of("timeout", duration), new Runnable() {
+            @Override public void run() {
+                assertTrue(events.isEmpty(), "events="+events);
+            }});
+    }
+    
+    private void assertNoEventsContinually() {
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                assertTrue(events.isEmpty(), "events="+events);
+            }});
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/ha/HaPolicyRebindTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/ha/HaPolicyRebindTest.java b/policy/src/test/java/org/apache/brooklyn/policy/ha/HaPolicyRebindTest.java
new file mode 100644
index 0000000..3782ccf
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/ha/HaPolicyRebindTest.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.fail;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.policy.EnricherSpec;
+import org.apache.brooklyn.api.policy.PolicySpec;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.rebind.RebindTestFixtureWithApp;
+
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+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.collect.Sets;
+
+public class HaPolicyRebindTest extends RebindTestFixtureWithApp {
+
+    private TestEntity origEntity;
+    private SensorEventListener<FailureDescriptor> eventListener;
+    private List<SensorEvent<FailureDescriptor>> events;
+    
+    @Override
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        super.setUp();
+        origEntity = origApp.createAndManageChild(EntitySpec.create(TestEntity.class));
+        events = Lists.newCopyOnWriteArrayList();
+        eventListener = new SensorEventListener<FailureDescriptor>() {
+            @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
+                events.add(event);
+            }
+        };
+    }
+
+    @Test
+    public void testServiceRestarterWorksAfterRebind() throws Exception {
+        origEntity.addPolicy(PolicySpec.create(ServiceRestarter.class)
+                .configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
+        
+        TestApplication newApp = rebind();
+        final TestEntity newEntity = (TestEntity) Iterables.find(newApp.getChildren(), Predicates.instanceOf(TestEntity.class));
+        
+        newEntity.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(origEntity, "simulate failure"));
+        
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                assertEquals(newEntity.getCallHistory(), ImmutableList.of("restart"));
+            }});
+    }
+
+    @Test
+    public void testServiceReplacerWorksAfterRebind() throws Exception {
+        Location origLoc = origManagementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
+        DynamicCluster origCluster = origApp.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(TestEntity.class))
+                .configure(DynamicCluster.INITIAL_SIZE, 3));
+        origApp.start(ImmutableList.<Location>of(origLoc));
+
+        origCluster.addPolicy(PolicySpec.create(ServiceReplacer.class)
+                .configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
+
+        // rebind
+        TestApplication newApp = rebind();
+        final DynamicCluster newCluster = (DynamicCluster) Iterables.find(newApp.getChildren(), Predicates.instanceOf(DynamicCluster.class));
+
+        // stimulate the policy
+        final Set<Entity> initialMembers = ImmutableSet.copyOf(newCluster.getMembers());
+        final TestEntity e1 = (TestEntity) Iterables.get(initialMembers, 1);
+        
+        newApp.getManagementContext().getSubscriptionManager().subscribe(e1, HASensors.ENTITY_FAILED, eventListener);
+        newApp.getManagementContext().getSubscriptionManager().subscribe(e1, HASensors.ENTITY_RECOVERED, eventListener);
+        
+        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
+        
+        // Expect e1 to be replaced
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                Set<Entity> newMembers = Sets.difference(ImmutableSet.copyOf(newCluster.getMembers()), initialMembers);
+                Set<Entity> removedMembers = Sets.difference(initialMembers, ImmutableSet.copyOf(newCluster.getMembers()));
+                assertEquals(removedMembers, ImmutableSet.of(e1));
+                assertEquals(newMembers.size(), 1);
+                assertEquals(((TestEntity)Iterables.getOnlyElement(newMembers)).getCallHistory(), ImmutableList.of("start"));
+                
+                // TODO e1 not reporting "start" after rebind because callHistory is a field rather than an attribute, so was not persisted
+                Asserts.assertEqualsIgnoringOrder(e1.getCallHistory(), ImmutableList.of("stop"));
+                assertFalse(Entities.isManaged(e1));
+            }});
+    }
+    
+    @Test
+    public void testServiceFailureDetectorWorksAfterRebind() throws Exception {
+        origEntity.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
+
+        // rebind
+        TestApplication newApp = rebind();
+        final TestEntity newEntity = (TestEntity) Iterables.find(newApp.getChildren(), Predicates.instanceOf(TestEntity.class));
+
+        newApp.getManagementContext().getSubscriptionManager().subscribe(newEntity, HASensors.ENTITY_FAILED, eventListener);
+
+        newEntity.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(newEntity, Lifecycle.RUNNING);
+        
+        // trigger the failure
+        newEntity.setAttribute(TestEntity.SERVICE_UP, false);
+
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(newEntity), null);
+        assertEquals(events.size(), 1, "events="+events);
+    }
+    
+    private void assertHasEventEventually(final Sensor<?> sensor, final Predicate<Object> componentPredicate, final Predicate<? super CharSequence> descriptionPredicate) {
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            @Override public void run() {
+                assertHasEvent(sensor, componentPredicate, descriptionPredicate);
+            }});
+    }
+    
+    private void assertHasEvent(Sensor<?> sensor, Predicate<Object> componentPredicate, Predicate<? super CharSequence> descriptionPredicate) {
+        for (SensorEvent<FailureDescriptor> event : events) {
+            if (event.getSensor().equals(sensor) && 
+                    (componentPredicate == null || componentPredicate.apply(event.getValue().getComponent())) &&
+                    (descriptionPredicate == null || descriptionPredicate.apply(event.getValue().getDescription()))) {
+                return;
+            }
+        }
+        fail("No matching "+sensor+" event found; events="+events);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceFailureDetectorStabilizationTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceFailureDetectorStabilizationTest.java b/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceFailureDetectorStabilizationTest.java
new file mode 100644
index 0000000..ca84592
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceFailureDetectorStabilizationTest.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.apache.brooklyn.api.policy.EnricherSpec;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
+import brooklyn.entity.basic.ServiceStateLogicTest;
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableMap;
+
+/** also see more primitive tests in {@link ServiceStateLogicTest} */
+public class ServiceFailureDetectorStabilizationTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ServiceFailureDetectorStabilizationTest.class);
+
+    private static final int TIMEOUT_MS = 10*1000;
+    private static final int OVERHEAD = 250;
+
+    private ManagementContext managementContext;
+    private TestApplication app;
+    private TestEntity e1;
+    
+    private List<SensorEvent<FailureDescriptor>> events;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        events = new CopyOnWriteArrayList<SensorEvent<FailureDescriptor>>();
+        
+        managementContext = new LocalManagementContextForTests();
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+        e1 = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        
+        app.getManagementContext().getSubscriptionManager().subscribe(
+                e1, 
+                HASensors.ENTITY_FAILED, 
+                new SensorEventListener<FailureDescriptor>() {
+                    @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
+                        events.add(event);
+                    }
+                });
+        app.getManagementContext().getSubscriptionManager().subscribe(
+                e1, 
+                HASensors.ENTITY_RECOVERED, 
+                new SensorEventListener<FailureDescriptor>() {
+                    @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
+                        events.add(event);
+                    }
+                });
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (managementContext != null) Entities.destroyAll(managementContext);
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testNotNotifiedOfTemporaryFailuresDuringStabilisationDelay() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+                .configure(ServiceFailureDetector.ENTITY_FAILED_STABILIZATION_DELAY, Duration.ONE_MINUTE));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+        Thread.sleep(100);
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+
+        assertNoEventsContinually();
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testNotifiedOfFailureAfterStabilisationDelay() throws Exception {
+        final int stabilisationDelay = 1000;
+        
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+                .configure(ServiceFailureDetector.ENTITY_FAILED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testFailuresThenUpDownResetsStabilisationCount() throws Exception {
+        LOG.debug("Running testFailuresThenUpDownResetsStabilisationCount");
+        final long stabilisationDelay = 1000;
+        
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+                .configure(ServiceFailureDetector.ENTITY_FAILED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        Thread.sleep(OVERHEAD);
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+        
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testNotNotifiedOfTemporaryRecoveryDuringStabilisationDelay() throws Exception {
+        final long stabilisationDelay = 1000;
+        
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+                .configure(ServiceFailureDetector.ENTITY_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+        events.clear();
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        Thread.sleep(100);
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+
+        assertNoEventsContinually(Duration.of(stabilisationDelay + OVERHEAD));
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testNotifiedOfRecoveryAfterStabilisationDelay() throws Exception {
+        final int stabilisationDelay = 1000;
+        
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+                .configure(ServiceFailureDetector.ENTITY_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+        events.clear();
+
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
+    }
+    
+    @Test(groups="Integration") // Because slow
+    public void testRecoversThenDownUpResetsStabilisationCount() throws Exception {
+        final long stabilisationDelay = 1000;
+        
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+                .configure(ServiceFailureDetector.ENTITY_RECOVERED_STABILIZATION_DELAY, Duration.of(stabilisationDelay)));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+        events.clear();
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+        Thread.sleep(OVERHEAD);
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        assertNoEventsContinually(Duration.of(stabilisationDelay - OVERHEAD));
+
+        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
+    }
+
+    private void assertHasEvent(Sensor<?> sensor, Predicate<Object> componentPredicate, Predicate<? super CharSequence> descriptionPredicate) {
+        for (SensorEvent<FailureDescriptor> event : events) {
+            if (event.getSensor().equals(sensor) && 
+                    (componentPredicate == null || componentPredicate.apply(event.getValue().getComponent())) &&
+                    (descriptionPredicate == null || descriptionPredicate.apply(event.getValue().getDescription()))) {
+                return;
+            }
+        }
+        fail("No matching "+sensor+" event found; events="+events);
+    }
+    
+    private void assertHasEventEventually(final Sensor<?> sensor, final Predicate<Object> componentPredicate, final Predicate<? super CharSequence> descriptionPredicate) {
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            @Override public void run() {
+                assertHasEvent(sensor, componentPredicate, descriptionPredicate);
+            }});
+    }
+
+    private void assertNoEventsContinually(Duration duration) {
+        Asserts.succeedsContinually(ImmutableMap.of("timeout", duration), new Runnable() {
+            @Override public void run() {
+                assertTrue(events.isEmpty(), "events="+events);
+            }});
+    }
+    
+    private void assertNoEventsContinually() {
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                assertTrue(events.isEmpty(), "events="+events);
+            }});
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceFailureDetectorTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceFailureDetectorTest.java b/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceFailureDetectorTest.java
new file mode 100644
index 0000000..2ad2d79
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceFailureDetectorTest.java
@@ -0,0 +1,407 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.apache.brooklyn.api.policy.EnricherSpec;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
+import brooklyn.entity.basic.ServiceStateLogic.ServiceProblemsLogic;
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableMap;
+
+public class ServiceFailureDetectorTest {
+    private static final Logger log = LoggerFactory.getLogger(ServiceFailureDetectorTest.class);
+
+    private static final int TIMEOUT_MS = 10*1000;
+
+    private ManagementContext managementContext;
+    private TestApplication app;
+    private TestEntity e1;
+    
+    private List<SensorEvent<FailureDescriptor>> events;
+    private SensorEventListener<FailureDescriptor> eventListener;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        events = new CopyOnWriteArrayList<SensorEvent<FailureDescriptor>>();
+        eventListener = new SensorEventListener<FailureDescriptor>() {
+            @Override public void onEvent(SensorEvent<FailureDescriptor> event) {
+                events.add(event);
+            }
+        };
+        
+        managementContext = new LocalManagementContextForTests();
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+        e1 = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        e1.addEnricher(ServiceStateLogic.newEnricherForServiceStateFromProblemsAndUp());
+        
+        app.getManagementContext().getSubscriptionManager().subscribe(e1, HASensors.ENTITY_FAILED, eventListener);
+        app.getManagementContext().getSubscriptionManager().subscribe(e1, HASensors.ENTITY_RECOVERED, eventListener);
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (managementContext != null) Entities.destroyAll(managementContext);
+    }
+    
+    @Test(groups="Integration") // Has a 1 second wait
+    public void testNotNotifiedOfFailuresForHealthy() throws Exception {
+        // Create members before and after the policy is registered, to test both scenarios
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
+        
+        assertNoEventsContinually();
+        assertEquals(e1.getAttribute(TestEntity.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING);
+    }
+    
+    @Test
+    public void testNotifiedOfFailure() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        
+        assertEquals(events.size(), 0, "events="+events);
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+        assertEquals(events.size(), 1, "events="+events);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+    }
+    
+    @Test
+    public void testNotifiedOfFailureOnProblem() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        
+        assertEquals(events.size(), 0, "events="+events);
+        
+        ServiceProblemsLogic.updateProblemsIndicator(e1, "test", "foo");
+
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+        assertEquals(events.size(), 1, "events="+events);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+    }
+    
+    @Test
+    public void testNotifiedOfFailureOnStateOnFire() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.ON_FIRE);
+
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+        assertEquals(events.size(), 1, "events="+events);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+    }
+    
+    @Test
+    public void testNotifiedOfRecovery() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        // Make the entity fail
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+
+        // And make the entity recover
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
+        assertEquals(events.size(), 2, "events="+events);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+    }
+    
+    @Test
+    public void testNotifiedOfRecoveryFromProblems() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        // Make the entity fail
+        ServiceProblemsLogic.updateProblemsIndicator(e1, "test", "foo");
+
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+
+        // And make the entity recover
+        ServiceProblemsLogic.clearProblemsIndicator(e1, "test");
+        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
+        assertEquals(events.size(), 2, "events="+events);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+    }
+    
+    
+    @Test(groups="Integration") // Has a 1 second wait
+    public void testEmitsEntityFailureOnlyIfPreviouslyUp() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
+        
+        // Make the entity fail
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+        assertNoEventsContinually();
+    }
+    
+    @Test
+    public void testDisablingPreviouslyUpRequirementForEntityFailed() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+            .configure(ServiceFailureDetector.ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP, false));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+    }
+    
+    @Test
+    public void testDisablingOnFire() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+            .configure(ServiceFailureDetector.SERVICE_ON_FIRE_STABILIZATION_DELAY, Duration.PRACTICALLY_FOREVER));
+        
+        // Make the entity fail
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+
+        assertEquals(e1.getAttribute(TestEntity.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING);
+    }
+    
+    @Test(groups="Integration") // Has a 1 second wait
+    public void testOnFireAfterDelay() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+            .configure(ServiceFailureDetector.SERVICE_ON_FIRE_STABILIZATION_DELAY, Duration.ONE_SECOND));
+        
+        // Make the entity fail
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+
+        assertEquals(e1.getAttribute(TestEntity.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING);
+        Time.sleep(Duration.millis(100));
+        assertEquals(e1.getAttribute(TestEntity.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+    }
+    
+    @Test(groups="Integration") // Has a 1 second wait
+    public void testOnFailureDelayFromProblemAndRecover() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+            .configure(ServiceFailureDetector.SERVICE_ON_FIRE_STABILIZATION_DELAY, Duration.ONE_SECOND)
+            .configure(ServiceFailureDetector.ENTITY_RECOVERED_STABILIZATION_DELAY, Duration.ONE_SECOND));
+        
+        // Set the entity to healthy
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+        
+        // Make the entity fail; won't set on-fire for 1s but will publish FAILED immediately.
+        ServiceStateLogic.ServiceProblemsLogic.updateProblemsIndicator(e1, "test", "foo");
+        EntityTestUtils.assertAttributeEqualsContinually(ImmutableMap.of("timeout", 100), e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+        assertEquals(e1.getAttribute(TestEntity.SERVICE_STATE_ACTUAL), Lifecycle.RUNNING);
+        
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+        
+        // Now recover: will publish RUNNING immediately, but has 1s stabilisation for RECOVERED
+        ServiceStateLogic.ServiceProblemsLogic.clearProblemsIndicator(e1, "test");
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+        
+        assertEquals(events.size(), 1, "events="+events);
+        
+        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
+        assertEquals(events.size(), 2, "events="+events);
+    }
+    
+    @Test(groups="Integration") // Has a 1 second wait
+    public void testAttendsToServiceState() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
+        
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        // not counted as failed because not expected to be running
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+
+        assertNoEventsContinually();
+    }
+
+    @Test(groups="Integration") // Has a 1 second wait
+    public void testOnlyReportsFailureIfRunning() throws Exception {
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class));
+        
+        // Make the entity fail
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.STARTING);
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+
+        assertNoEventsContinually();
+    }
+    
+    @Test
+    public void testReportsFailureWhenAlreadyDownOnRegisteringPolicy() throws Exception {
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        e1.setAttribute(TestEntity.SERVICE_UP, false);
+
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+            .configure(ServiceFailureDetector.ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP, false));
+
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+    }
+    
+    @Test
+    public void testReportsFailureWhenAlreadyOnFireOnRegisteringPolicy() throws Exception {
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.ON_FIRE);
+
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+            .configure(ServiceFailureDetector.ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP, false));
+
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+    }
+    
+    @Test(groups="Integration") // Has a 1.5 second wait
+    public void testRepublishedFailure() throws Exception {
+        Duration republishPeriod = Duration.millis(100);
+
+        e1.addEnricher(EnricherSpec.create(ServiceFailureDetector.class)
+                .configure(ServiceFailureDetector.ENTITY_FAILED_REPUBLISH_TIME, republishPeriod));
+            
+        // Set the entity to healthy
+        e1.setAttribute(TestEntity.SERVICE_UP, true);
+        ServiceStateLogic.setExpectedState(e1, Lifecycle.RUNNING);
+        EntityTestUtils.assertAttributeEqualsEventually(e1, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+        
+        // Make the entity fail;
+        ServiceStateLogic.ServiceProblemsLogic.updateProblemsIndicator(e1, "test", "foo");
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+        assertHasEventEventually(HASensors.ENTITY_FAILED, Predicates.<Object>equalTo(e1), null);
+
+        //wait for at least 10 republish events (~1 sec)
+        assertEventsSizeEventually(10);
+
+        // Now recover
+        ServiceStateLogic.ServiceProblemsLogic.clearProblemsIndicator(e1, "test");
+        EntityTestUtils.assertAttributeEqualsEventually(e1, TestEntity.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+        assertHasEventEventually(HASensors.ENTITY_RECOVERED, Predicates.<Object>equalTo(e1), null);
+
+        //once recovered check no more failed events emitted periodically
+        assertEventsSizeContiniually(events.size());
+
+        SensorEvent<FailureDescriptor> prevEvent = null;
+        for (SensorEvent<FailureDescriptor> event : events) {
+            if (prevEvent != null) {
+                long repeatOffset = event.getTimestamp() - prevEvent.getTimestamp();
+                long deviation = Math.abs(repeatOffset - republishPeriod.toMilliseconds());
+                if (deviation > republishPeriod.toMilliseconds()/10 &&
+                        //warn only if recovered is too far away from the last failure
+                        (!event.getSensor().equals(HASensors.ENTITY_RECOVERED) ||
+                        repeatOffset > republishPeriod.toMilliseconds())) {
+                    log.error("The time between failure republish (" + repeatOffset + "ms) deviates too much from the expected " + republishPeriod + ". prevEvent=" + prevEvent + ", event=" + event);
+                }
+            }
+            prevEvent = event;
+        }
+        
+        //make sure no republish takes place after recovered
+        assertEquals(prevEvent.getSensor(), HASensors.ENTITY_RECOVERED);
+    }
+    
+    private void assertEventsSizeContiniually(final int size) {
+        Asserts.succeedsContinually(MutableMap.of("timeout", 500), new Runnable() {
+            @Override
+            public void run() {
+                assertTrue(events.size() == size, "assertEventsSizeContiniually expects " + size + " events but found " + events.size() + ": " + events);
+            }
+        });
+    }
+
+    private void assertEventsSizeEventually(final int size) {
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            @Override
+            public void run() {
+                assertTrue(events.size() >= size, "assertEventsSizeContiniually expects at least " + size + " events but found " + events.size() + ": " + events);
+            }
+        });
+    }
+
+    private void assertHasEvent(Sensor<?> sensor, Predicate<Object> componentPredicate, Predicate<? super CharSequence> descriptionPredicate) {
+        for (SensorEvent<FailureDescriptor> event : events) {
+            if (event.getSensor().equals(sensor) && 
+                    (componentPredicate == null || componentPredicate.apply(event.getValue().getComponent())) &&
+                    (descriptionPredicate == null || descriptionPredicate.apply(event.getValue().getDescription()))) {
+                return;
+            }
+        }
+        fail("No matching "+sensor+" event found; events="+events);
+    }
+    
+    private void assertHasEventEventually(final Sensor<?> sensor, final Predicate<Object> componentPredicate, final Predicate<? super CharSequence> descriptionPredicate) {
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            @Override public void run() {
+                assertHasEvent(sensor, componentPredicate, descriptionPredicate);
+            }});
+    }
+    
+    private void assertNoEventsContinually() {
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                assertTrue(events.isEmpty(), "events="+events);
+            }});
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceReplacerTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceReplacerTest.java b/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceReplacerTest.java
new file mode 100644
index 0000000..f92603a
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceReplacerTest.java
@@ -0,0 +1,340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.apache.brooklyn.api.policy.PolicySpec;
+import org.apache.brooklyn.core.util.config.ConfigBag;
+import org.apache.brooklyn.test.EntityTestUtils;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.QuorumCheck;
+import brooklyn.entity.basic.ServiceStateLogic.ComputeServiceIndicatorsFromChildrenAndMembers;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.trait.FailingEntity;
+
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+import brooklyn.test.Asserts;
+import brooklyn.util.javalang.JavaClassNames;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+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.collect.Sets;
+
+public class ServiceReplacerTest {
+
+    private static final Logger log = LoggerFactory.getLogger(ServiceReplacerTest.class);
+    
+    private ManagementContext managementContext;
+    private TestApplication app;
+    private SimulatedLocation loc;
+    private SensorEventListener<Object> eventListener;
+    private List<SensorEvent<?>> events;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        managementContext = new LocalManagementContextForTests();
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+        loc = managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
+        events = Lists.newCopyOnWriteArrayList();
+        eventListener = new SensorEventListener<Object>() {
+            @Override public void onEvent(SensorEvent<Object> event) {
+                events.add(event);
+            }
+        };
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (managementContext != null) Entities.destroyAll(managementContext);
+    }
+    
+    @Test
+    public void testReplacesFailedMember() throws Exception {
+        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(TestEntity.class))
+                .configure(DynamicCluster.INITIAL_SIZE, 3));
+        app.start(ImmutableList.<Location>of(loc));
+
+        ServiceReplacer policy = new ServiceReplacer(new ConfigBag().configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
+        cluster.addPolicy(policy);
+
+        final Set<Entity> initialMembers = ImmutableSet.copyOf(cluster.getMembers());
+        final TestEntity e1 = (TestEntity) Iterables.get(initialMembers, 1);
+        
+        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
+        
+        // Expect e1 to be replaced
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                Set<Entity> newMembers = Sets.difference(ImmutableSet.copyOf(cluster.getMembers()), initialMembers);
+                Set<Entity> removedMembers = Sets.difference(initialMembers, ImmutableSet.copyOf(cluster.getMembers()));
+                assertEquals(removedMembers, ImmutableSet.of(e1));
+                assertEquals(newMembers.size(), 1);
+                assertEquals(((TestEntity)Iterables.getOnlyElement(newMembers)).getCallHistory(), ImmutableList.of("start"));
+                assertEquals(e1.getCallHistory(), ImmutableList.of("start", "stop"));
+                assertFalse(Entities.isManaged(e1));
+            }});
+    }
+
+    @Test(invocationCount=100)
+    public void testSetsOnFireWhenFailToReplaceMemberManyTimes() throws Exception {
+        testSetsOnFireWhenFailToReplaceMember();
+    }
+    
+    // fails the startup of the replacement entity (but not the original). 
+    @Test
+    public void testSetsOnFireWhenFailToReplaceMember() throws Exception {
+        app.subscribe(null, ServiceReplacer.ENTITY_REPLACEMENT_FAILED, eventListener);
+        
+        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(FailingEntity.class)
+                        .configure(FailingEntity.FAIL_ON_START_CONDITION, predicateOnlyTrueForCallAtOrAfter(2)))
+                .configure(DynamicCluster.INITIAL_SIZE, 1)
+                .configure(DynamicCluster.QUARANTINE_FAILED_ENTITIES, true)
+                .configure(ComputeServiceIndicatorsFromChildrenAndMembers.UP_QUORUM_CHECK, QuorumCheck.QuorumChecks.alwaysTrue())
+                .configure(ComputeServiceIndicatorsFromChildrenAndMembers.RUNNING_QUORUM_CHECK, QuorumCheck.QuorumChecks.alwaysTrue()));
+        app.start(ImmutableList.<Location>of(loc));
+        
+        // should not be on fire
+        Assert.assertNotEquals(cluster.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
+        // and should eventually be running
+        EntityTestUtils.assertAttributeEqualsEventually(cluster, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
+        
+        log.info("started "+app+" for "+JavaClassNames.niceClassAndMethod());
+        
+        ServiceReplacer policy = new ServiceReplacer(new ConfigBag().configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
+        cluster.addPolicy(policy);
+        
+        final Set<Entity> initialMembers = ImmutableSet.copyOf(cluster.getMembers());
+        final TestEntity e1 = (TestEntity) Iterables.get(initialMembers, 0);
+        
+        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
+
+        // Expect cluster to go on-fire when fails to start replacement
+        // Note that we've set up-quorum and running-quorum to be "alwaysTrue" so that we don't get a transient onFire
+        // when the failed node fails to start (but before it has been removed from the group to be put in quarantine).
+        EntityTestUtils.assertAttributeEqualsEventually(cluster, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+
+        // Expect to have the second failed entity still kicking around as proof (in quarantine)
+        // The cluster should NOT go on fire until after the 2nd failure
+        Iterable<Entity> members = Iterables.filter(managementContext.getEntityManager().getEntities(), Predicates.instanceOf(FailingEntity.class));
+        assertEquals(Iterables.size(members), 2);
+
+        // e2 failed to start, so it won't have called stop on e1
+        TestEntity e2 = (TestEntity) Iterables.getOnlyElement(Sets.difference(ImmutableSet.copyOf(members), initialMembers));
+        assertEquals(e1.getCallHistory(), ImmutableList.of("start"), "e1.history="+e1.getCallHistory());
+        assertEquals(e2.getCallHistory(), ImmutableList.of("start"), "e2.history="+e2.getCallHistory());
+
+        // And will have received notification event about it
+        assertEventuallyHasEntityReplacementFailedEvent(cluster);
+    }
+    
+    @Test(groups="Integration") // has a 1 second wait
+    public void testDoesNotOnFireWhenFailToReplaceMember() throws Exception {
+        app.subscribe(null, ServiceReplacer.ENTITY_REPLACEMENT_FAILED, eventListener);
+        
+        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(FailingEntity.class)
+                        .configure(FailingEntity.FAIL_ON_START_CONDITION, predicateOnlyTrueForCallAtOrAfter(2)))
+                .configure(DynamicCluster.INITIAL_SIZE, 1)
+                .configure(DynamicCluster.QUARANTINE_FAILED_ENTITIES, true));
+        app.start(ImmutableList.<Location>of(loc));
+        
+        ServiceReplacer policy = new ServiceReplacer(new ConfigBag()
+                .configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED)
+                .configure(ServiceReplacer.SET_ON_FIRE_ON_FAILURE, false));
+        cluster.addPolicy(policy);
+        
+        final Set<Entity> initialMembers = ImmutableSet.copyOf(cluster.getMembers());
+        final TestEntity e1 = (TestEntity) Iterables.get(initialMembers, 0);
+        
+        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
+
+        // Configured to not mark cluster as on fire
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                assertNotEquals(cluster.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
+            }});
+        
+        // And will have received notification event about it
+        assertEventuallyHasEntityReplacementFailedEvent(cluster);
+    }
+
+    @Test(groups="Integration")  // 1s wait
+    public void testStopFailureOfOldEntityDoesNotSetClusterOnFire() throws Exception {
+        app.subscribe(null, ServiceReplacer.ENTITY_REPLACEMENT_FAILED, eventListener);
+        
+        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(FailingEntity.class)
+                        .configure(FailingEntity.FAIL_ON_STOP_CONDITION, predicateOnlyTrueForCallAt(1)))
+                .configure(DynamicCluster.INITIAL_SIZE, 2));
+        app.start(ImmutableList.<Location>of(loc));
+        
+        cluster.addPolicy(PolicySpec.create(ServiceReplacer.class)
+                .configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
+        
+        final Set<Entity> initialMembers = ImmutableSet.copyOf(cluster.getMembers());
+        final TestEntity e1 = (TestEntity) Iterables.get(initialMembers, 0);
+        
+        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
+
+        // Expect e1 to be replaced
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                Set<Entity> newMembers = Sets.difference(ImmutableSet.copyOf(cluster.getMembers()), initialMembers);
+                Set<Entity> removedMembers = Sets.difference(initialMembers, ImmutableSet.copyOf(cluster.getMembers()));
+                assertEquals(removedMembers, ImmutableSet.of(e1));
+                assertEquals(newMembers.size(), 1);
+                assertEquals(((TestEntity)Iterables.getOnlyElement(newMembers)).getCallHistory(), ImmutableList.of("start"));
+                assertEquals(e1.getCallHistory(), ImmutableList.of("start", "stop"));
+                assertFalse(Entities.isManaged(e1));
+            }});
+
+        // Failure to stop the failed member should not cause "on-fire" of cluster
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                assertNotEquals(cluster.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
+            }});
+    }
+
+    /**
+     * If we keep on getting failure reports, never managing to replace the failed node, then don't keep trying
+     * (i.e. avoid infinite loop).
+     * 
+     * TODO This code + configuration needs some work; it's not testing quite the scenarios that I
+     * was thinking of!
+     * I saw problem where a node failed, and the replacements failed, and we ended up trying thousands of times.
+     * (describing this scenario is made more complex by me having temporarily disabled the cluster from 
+     * removing failed members, for debugging purposes!)
+     * Imagine these two scenarios:
+     * <ol>
+     *   <li>Entity fails during call to start().
+     *       Here, the cluster removes it as a member (either unmanages it or puts it in quarantine)
+     *       So the ENTITY_FAILED is ignored because the entity is not a member at that point.
+     *   <li>Entity returns from start(), but quickly goes to service-down.
+     *       Here we'll keep trying to replace that entity. Depending how long that takes, we'll either 
+     *       enter a horrible infinite loop, or we'll just provision a huge number of VMs over a long 
+     *       time period.
+     *       Unfortunately this scenario is not catered for in the code yet.
+     * </ol>
+     */
+    @Test(groups="Integration") // because takes 1.2 seconds
+    public void testAbandonsReplacementAfterNumFailures() throws Exception {
+        app.subscribe(null, ServiceReplacer.ENTITY_REPLACEMENT_FAILED, eventListener);
+        
+        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(FailingEntity.class)
+                        .configure(FailingEntity.FAIL_ON_START_CONDITION, predicateOnlyTrueForCallAtOrAfter(11)))
+                .configure(DynamicCluster.INITIAL_SIZE, 10)
+                .configure(DynamicCluster.QUARANTINE_FAILED_ENTITIES, true));
+        app.start(ImmutableList.<Location>of(loc));
+        
+        ServiceReplacer policy = new ServiceReplacer(new ConfigBag()
+                .configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED)
+                .configure(ServiceReplacer.FAIL_ON_NUM_RECURRING_FAILURES, 3));
+        cluster.addPolicy(policy);
+
+        final Set<Entity> initialMembers = ImmutableSet.copyOf(cluster.getMembers());
+        for (int i = 0; i < 5; i++) {
+            final int counter = i+1;
+            EntityInternal entity = (EntityInternal) Iterables.get(initialMembers, i);
+            entity.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(entity, "simulate failure"));
+            if (i <= 3) {
+                Asserts.succeedsEventually(new Runnable() {
+                    @Override public void run() {
+                        Set<FailingEntity> all = ImmutableSet.copyOf(Iterables.filter(managementContext.getEntityManager().getEntities(), FailingEntity.class));
+                        Set<FailingEntity> replacements = Sets.difference(all, initialMembers);
+                        Set<?> replacementMembers = Sets.intersection(ImmutableSet.of(cluster.getMembers()), replacements);
+                        assertTrue(replacementMembers.isEmpty());
+                        assertEquals(replacements.size(), counter);
+                    }});
+            } else {
+                Asserts.succeedsContinually(new Runnable() {
+                    @Override public void run() {
+                        Set<FailingEntity> all = ImmutableSet.copyOf(Iterables.filter(managementContext.getEntityManager().getEntities(), FailingEntity.class));
+                        Set<FailingEntity> replacements = Sets.difference(all, initialMembers);
+                        assertEquals(replacements.size(), 4);
+                    }});
+            }
+        }
+    }
+
+
+    private Predicate<Object> predicateOnlyTrueForCallAt(final int callNumber) {
+        return predicateOnlyTrueForCallRange(callNumber, callNumber);
+    }
+
+    private Predicate<Object> predicateOnlyTrueForCallAtOrAfter(final int callLowerNumber) {
+        return predicateOnlyTrueForCallRange(callLowerNumber, Integer.MAX_VALUE);
+    }
+    
+    private Predicate<Object> predicateOnlyTrueForCallRange(final int callLowerNumber, final int callUpperNumber) {
+        return new Predicate<Object>() {
+            private final AtomicInteger counter = new AtomicInteger(0);
+            @Override public boolean apply(Object input) {
+                int num = counter.incrementAndGet();
+                return num >= callLowerNumber && num <= callUpperNumber;
+            }
+        };
+    }
+
+    private void assertEventuallyHasEntityReplacementFailedEvent(final Entity expectedCluster) {
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                assertEquals(Iterables.getOnlyElement(events).getSensor(), ServiceReplacer.ENTITY_REPLACEMENT_FAILED, "events="+events);
+                assertEquals(Iterables.getOnlyElement(events).getSource(), expectedCluster, "events="+events);
+                assertEquals(((FailureDescriptor)Iterables.getOnlyElement(events).getValue()).getComponent(), expectedCluster, "events="+events);
+            }});
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceRestarterTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceRestarterTest.java b/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceRestarterTest.java
new file mode 100644
index 0000000..c93612b
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/ha/ServiceRestarterTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.apache.brooklyn.api.policy.PolicySpec;
+import org.apache.brooklyn.core.util.config.ConfigBag;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestEntity;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.trait.FailingEntity;
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+import brooklyn.test.Asserts;
+import brooklyn.util.exceptions.Exceptions;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+public class ServiceRestarterTest {
+
+    private static final int TIMEOUT_MS = 10*1000;
+
+    private ManagementContext managementContext;
+    private TestApplication app;
+    private TestEntity e1;
+    private ServiceRestarter policy;
+    private SensorEventListener<Object> eventListener;
+    private List<SensorEvent<?>> events;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        managementContext = new LocalManagementContextForTests();
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+        e1 = app.createAndManageChild(EntitySpec.create(TestEntity.class));
+        events = Lists.newCopyOnWriteArrayList();
+        eventListener = new SensorEventListener<Object>() {
+            @Override public void onEvent(SensorEvent<Object> event) {
+                events.add(event);
+            }
+        };
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (managementContext != null) Entities.destroyAll(managementContext);
+    }
+    
+    @Test
+    public void testRestartsOnFailure() throws Exception {
+        policy = new ServiceRestarter(new ConfigBag().configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
+        e1.addPolicy(policy);
+        
+        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
+        
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                assertEquals(e1.getCallHistory(), ImmutableList.of("restart"));
+            }});
+    }
+    
+    @Test(groups="Integration") // Has a 1 second wait
+    public void testDoesNotRestartsWhenHealthy() throws Exception {
+        policy = new ServiceRestarter(new ConfigBag().configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
+        e1.addPolicy(policy);
+        
+        e1.emit(HASensors.ENTITY_RECOVERED, new FailureDescriptor(e1, "not a failure"));
+        
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                assertEquals(e1.getCallHistory(), ImmutableList.of());
+            }});
+    }
+    
+    @Test
+    public void testEmitsFailureEventWhenRestarterFails() throws Exception {
+        final FailingEntity e2 = app.createAndManageChild(EntitySpec.create(FailingEntity.class)
+                .configure(FailingEntity.FAIL_ON_RESTART, true));
+        app.subscribe(e2, ServiceRestarter.ENTITY_RESTART_FAILED, eventListener);
+
+        policy = new ServiceRestarter(new ConfigBag().configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
+        e2.addPolicy(policy);
+
+        e2.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e2, "simulate failure"));
+        
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                assertEquals(Iterables.getOnlyElement(events).getSensor(), ServiceRestarter.ENTITY_RESTART_FAILED, "events="+events);
+                assertEquals(Iterables.getOnlyElement(events).getSource(), e2, "events="+events);
+                assertEquals(((FailureDescriptor)Iterables.getOnlyElement(events).getValue()).getComponent(), e2, "events="+events);
+            }});
+        
+        assertEquals(e2.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
+    }
+    
+    @Test
+    public void testDoesNotSetOnFireOnFailure() throws Exception {
+        final FailingEntity e2 = app.createAndManageChild(EntitySpec.create(FailingEntity.class)
+                .configure(FailingEntity.FAIL_ON_RESTART, true));
+        app.subscribe(e2, ServiceRestarter.ENTITY_RESTART_FAILED, eventListener);
+
+        policy = new ServiceRestarter(new ConfigBag()
+                .configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED)
+                .configure(ServiceRestarter.SET_ON_FIRE_ON_FAILURE, false));
+        e2.addPolicy(policy);
+
+        e2.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e2, "simulate failure"));
+        
+        Asserts.succeedsContinually(new Runnable() {
+            @Override public void run() {
+                assertNotEquals(e2.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
+            }});
+    }
+    
+    // Previously RestarterPolicy called entity.restart inside the event-listener thread.
+    // That caused all other events for that entity's subscriptions to be queued until that
+    // entity's single event handler thread was free again.
+    @Test
+    public void testRestartDoesNotBlockOtherSubscriptions() throws Exception {
+        final CountDownLatch inRestartLatch = new CountDownLatch(1);
+        final CountDownLatch continueRestartLatch = new CountDownLatch(1);
+        
+        final FailingEntity e2 = app.createAndManageChild(EntitySpec.create(FailingEntity.class)
+                .configure(FailingEntity.FAIL_ON_RESTART, true)
+                .configure(FailingEntity.EXEC_ON_FAILURE, new Function<Object, Void>() {
+                    @Override public Void apply(Object input) {
+                        inRestartLatch.countDown();
+                        try {
+                            continueRestartLatch.await();
+                        } catch (InterruptedException e) {
+                            throw Exceptions.propagate(e);
+                        }
+                        return null;
+                    }}));
+        
+        e2.addPolicy(PolicySpec.create(ServiceRestarter.class)
+                .configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
+        e2.subscribe(e2, TestEntity.SEQUENCE, eventListener);
+
+        // Cause failure, and wait for entity.restart to be blocking
+        e2.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
+        assertTrue(inRestartLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        
+        // Expect other notifications to continue to get through
+        e2.setAttribute(TestEntity.SEQUENCE, 1);
+        Asserts.succeedsEventually(new Runnable() {
+            @Override public void run() {
+                assertEquals(Iterables.getOnlyElement(events).getValue(), 1);
+            }});
+
+        // Allow restart to finish
+        continueRestartLatch.countDown();
+    }
+}


[06/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/AbstractLoadBalancingPolicyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/AbstractLoadBalancingPolicyTest.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/AbstractLoadBalancingPolicyTest.java
new file mode 100644
index 0000000..6257bc9
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/AbstractLoadBalancingPolicyTest.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.event.AttributeSensor;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.DynamicGroup;
+import brooklyn.entity.basic.Entities;
+import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.event.basic.Sensors;
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+public class AbstractLoadBalancingPolicyTest {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractLoadBalancingPolicyTest.class);
+    
+    protected static final long TIMEOUT_MS = 10*1000;
+    protected static final long SHORT_WAIT_MS = 250;
+    
+    protected static final long CONTAINER_STARTUP_DELAY_MS = 100;
+    
+    public static final AttributeSensor<Integer> TEST_METRIC =
+        Sensors.newIntegerSensor("test.metric", "Dummy workrate for test entities");
+    
+    public static final ConfigKey<Double> LOW_THRESHOLD_CONFIG_KEY = new BasicConfigKey<Double>(Double.class, TEST_METRIC.getName()+".threshold.low", "desc", 0.0);
+    public static final ConfigKey<Double> HIGH_THRESHOLD_CONFIG_KEY = new BasicConfigKey<Double>(Double.class, TEST_METRIC.getName()+".threshold.high", "desc", 0.0);
+    
+    protected TestApplication app;
+    protected SimulatedLocation loc;
+    protected BalanceableWorkerPool pool;
+    protected DefaultBalanceablePoolModel<Entity, Entity> model;
+    protected LoadBalancingPolicy policy;
+    protected Group containerGroup;
+    protected Group itemGroup;
+    protected Random random = new Random();
+    
+    @BeforeMethod(alwaysRun=true)
+    public void before() {
+        LOG.debug("In AbstractLoadBalancingPolicyTest.before()");
+        
+        MockItemEntityImpl.totalMoveCount.set(0);
+        MockItemEntityImpl.lastMoveTime.set(0);
+        
+        loc = new SimulatedLocation(MutableMap.of("name", "loc"));
+        
+        model = new DefaultBalanceablePoolModel<Entity, Entity>("pool-model");
+        
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        containerGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
+                .displayName("containerGroup")
+                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockContainerEntity.class)));
+        itemGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
+                .displayName("itemGroup")
+                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockItemEntity.class)));
+        pool = app.createAndManageChild(EntitySpec.create(BalanceableWorkerPool.class));
+        pool.setContents(containerGroup, itemGroup);
+        policy = new LoadBalancingPolicy(MutableMap.of("minPeriodBetweenExecs", 1), TEST_METRIC, model);
+        pool.addPolicy(policy);
+        app.start(ImmutableList.of(loc));
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void after() {
+        if (policy != null) policy.destroy();
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+    
+    // Using this utility, as it gives more info about the workrates of all containers rather than just the one that differs    
+    protected void assertWorkrates(Collection<MockContainerEntity> containers, Collection<Double> expectedC, double precision) {
+        Iterable<Double> actual = Iterables.transform(containers, new Function<MockContainerEntity, Double>() {
+            public Double apply(MockContainerEntity input) {
+                return getContainerWorkrate(input);
+            }});
+        
+        List<Double> expected = Lists.newArrayList(expectedC);
+        String errMsg = "actual="+actual+"; expected="+expected;
+        assertEquals(containers.size(), expected.size(), errMsg);
+        for (int i = 0; i < containers.size(); i++) {
+            assertEquals(Iterables.get(actual, i), expected.get(i), precision, errMsg);
+        }
+    }
+    
+    protected void assertWorkratesEventually(Collection<MockContainerEntity> containers, Iterable<? extends Movable> items, Collection<Double> expected) {
+        assertWorkratesEventually(containers, items, expected, 0d);
+    }
+
+    /**
+     * Asserts that the given container have the given expected workrates (by querying the containers directly).
+     * Accepts an accuracy of "precision" for each container's workrate.
+     */
+    protected void assertWorkratesEventually(final Collection<MockContainerEntity> containers, final Iterable<? extends Movable> items, final Collection<Double> expected, final double precision) {
+        try {
+            Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+                public void run() {
+                    assertWorkrates(containers, expected, precision);
+                }});
+        } catch (AssertionError e) {
+            String errMsg = e.getMessage()+"; "+verboseDumpToString(containers, items);
+            throw new RuntimeException(errMsg, e);
+        }
+    }
+
+    // Using this utility, as it gives more info about the workrates of all containers rather than just the one that differs    
+    protected void assertWorkratesContinually(List<MockContainerEntity> containers, Iterable<? extends Movable> items, List<Double> expected) {
+        assertWorkratesContinually(containers, items, expected, 0d);
+    }
+
+    /**
+     * Asserts that the given containers have the given expected workrates (by querying the containers directly)
+     * continuously for SHORT_WAIT_MS.
+     * Accepts an accuracy of "precision" for each container's workrate.
+     */
+    protected void assertWorkratesContinually(final List<MockContainerEntity> containers, Iterable<? extends Movable> items, final List<Double> expected, final double precision) {
+        try {
+            Asserts.succeedsContinually(MutableMap.of("timeout", SHORT_WAIT_MS), new Runnable() {
+                public void run() {
+                    assertWorkrates(containers, expected, precision);
+                }});
+        } catch (AssertionError e) {
+            String errMsg = e.getMessage()+"; "+verboseDumpToString(containers, items);
+            throw new RuntimeException(errMsg, e);
+        }
+    }
+
+    protected String verboseDumpToString(Iterable<MockContainerEntity> containers, Iterable<? extends Movable> items) {
+        Iterable<Double> containerRates = Iterables.transform(containers, new Function<MockContainerEntity, Double>() {
+            @Override public Double apply(MockContainerEntity input) {
+                return (double) input.getWorkrate();
+            }});
+        
+        Map<MockContainerEntity, Set<Movable>> itemDistributionByContainer = Maps.newLinkedHashMap();
+        for (MockContainerEntity container : containers) {
+            itemDistributionByContainer.put(container, container.getBalanceableItems());
+        }
+        
+        Map<Movable, BalanceableContainer<?>> itemDistributionByItem = Maps.newLinkedHashMap();
+        for (Movable item : items) {
+            itemDistributionByItem.put(item, item.getAttribute(Movable.CONTAINER));
+        }
+
+        String modelItemDistribution = model.itemDistributionToString();
+        return "containers="+containers+"; containerRates="+containerRates
+                +"; itemDistributionByContainer="+itemDistributionByContainer
+                +"; itemDistributionByItem="+itemDistributionByItem
+                +"; model="+modelItemDistribution
+                +"; totalMoves="+MockItemEntityImpl.totalMoveCount
+                +"; lastMoveTime="+Time.makeDateString(MockItemEntityImpl.lastMoveTime.get());
+    }
+    
+    protected MockContainerEntity newContainer(TestApplication app, String name, double lowThreshold, double highThreshold) {
+        return newAsyncContainer(app, name, lowThreshold, highThreshold, 0);
+    }
+    
+    /**
+     * Creates a new container that will take "delay" millis to complete its start-up.
+     */
+    protected MockContainerEntity newAsyncContainer(TestApplication app, String name, double lowThreshold, double highThreshold, long delay) {
+        MockContainerEntity container = app.createAndManageChild(EntitySpec.create(MockContainerEntity.class)
+                .displayName(name)
+                .configure(MockContainerEntity.DELAY, delay)
+                .configure(LOW_THRESHOLD_CONFIG_KEY, lowThreshold)
+                .configure(HIGH_THRESHOLD_CONFIG_KEY, highThreshold));
+        LOG.debug("Managed new container {}", container);
+        container.start(ImmutableList.of(loc));
+        return container;
+    }
+    
+    protected static MockItemEntity newItem(TestApplication app, MockContainerEntity container, String name, double workrate) {
+        MockItemEntity item = app.createAndManageChild(EntitySpec.create(MockItemEntity.class)
+                .displayName(name));
+        LOG.debug("Managing new item {} on container {}", item, container);
+        item.move(container);
+        ((EntityLocal)item).setAttribute(TEST_METRIC, (int)workrate);
+        return item;
+    }
+    
+    protected static MockItemEntity newLockedItem(TestApplication app, MockContainerEntity container, String name, double workrate) {
+        MockItemEntity item = app.createAndManageChild(EntitySpec.create(MockItemEntity.class)
+                .displayName(name)
+                .configure(Movable.IMMOVABLE, true));
+        LOG.debug("Managed new item {} on container {}", item, container);
+        item.move(container);
+        ((EntityLocal)item).setAttribute(TEST_METRIC, (int)workrate);
+        return item;
+    }
+    
+    /**
+     * Asks the item directly for its workrate.
+     */
+    protected static double getItemWorkrate(MockItemEntity item) {
+        Object result = item.getAttribute(TEST_METRIC);
+        return (result == null ? 0 : ((Number) result).doubleValue());
+    }
+    
+    /**
+     * Asks the container for its items, and then each of those items directly for their workrates; returns the total.
+     */
+    protected static double getContainerWorkrate(MockContainerEntity container) {
+        double result = 0.0;
+        Preconditions.checkNotNull(container, "container");
+        for (Movable item : container.getBalanceableItems()) {
+            Preconditions.checkNotNull(item, "item in container");
+            assertEquals(item.getContainerId(), container.getId());
+            result += getItemWorkrate((MockItemEntity)item);
+        }
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPoolTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPoolTest.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPoolTest.java
new file mode 100644
index 0000000..18f6847
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/BalanceableWorkerPoolTest.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.AbstractGroup;
+import brooklyn.entity.basic.AbstractGroupImpl;
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.DynamicGroup;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.trait.Resizable;
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.Exceptions;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+
+public class BalanceableWorkerPoolTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(BalanceableWorkerPoolTest.class);
+    
+    protected static final long TIMEOUT_MS = 10*1000;
+    protected static final long SHORT_WAIT_MS = 250;
+    
+    protected static final long CONTAINER_STARTUP_DELAY_MS = 100;
+    
+    protected TestApplication app;
+    protected SimulatedLocation loc;
+    protected BalanceableWorkerPool pool;
+    protected Group containerGroup;
+    protected Group itemGroup;
+    
+    @BeforeMethod(alwaysRun=true)
+    public void before() {
+        loc = new SimulatedLocation(MutableMap.of("name", "loc"));
+        
+        app = ApplicationBuilder.newManagedApp(TestApplication.class);
+        containerGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
+                .displayName("containerGroup")
+                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockContainerEntity.class)));
+        itemGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
+                .displayName("itemGroup")
+                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockItemEntity.class)));
+        pool = app.createAndManageChild(EntitySpec.create(BalanceableWorkerPool.class));
+        pool.setContents(containerGroup, itemGroup);
+        
+        app.start(ImmutableList.of(loc));
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void after() {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+    
+    @Test
+    public void testDefaultResizeFailsIfContainerGroupNotResizable() throws Exception {
+        try {
+            pool.resize(1);
+            fail();
+        } catch (Exception e) {
+            if (Exceptions.getFirstThrowableOfType(e, UnsupportedOperationException.class) == null) throw e;
+        }
+    }
+    
+    @Test
+    public void testDefaultResizeCallsResizeOnContainerGroup() {
+        LocallyResizableGroup resizable = app.createAndManageChild(EntitySpec.create(LocallyResizableGroup.class));
+        
+        BalanceableWorkerPool pool2 = app.createAndManageChild(EntitySpec.create(BalanceableWorkerPool.class));
+        pool2.setContents(resizable, itemGroup);
+        Entities.manage(pool2);
+        
+        pool2.resize(123);
+        assertEquals(resizable.getCurrentSize(), (Integer) 123);
+    }
+    
+    @Test
+    public void testCustomResizableCalledWhenResizing() {
+        LocallyResizableGroup resizable = app.createAndManageChild(EntitySpec.create(LocallyResizableGroup.class));
+        
+        pool.setResizable(resizable);
+        
+        pool.resize(123);
+        assertEquals(resizable.getCurrentSize(), (Integer)123);
+    }
+
+    @ImplementedBy(LocallyResizableGroupImpl.class)
+    public static interface LocallyResizableGroup extends AbstractGroup, Resizable {
+    }
+    
+    public static class LocallyResizableGroupImpl extends AbstractGroupImpl implements LocallyResizableGroup {
+        private int size = 0;
+
+        @Override
+        public Integer resize(Integer newSize) {
+            size = newSize;
+            return size;
+        }
+        @Override
+        public Integer getCurrentSize() {
+            return size;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroupTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroupTest.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroupTest.java
new file mode 100644
index 0000000..c256334
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroupTest.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import static org.testng.Assert.assertEquals;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.DynamicGroup;
+import brooklyn.entity.basic.Entities;
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+public class ItemsInContainersGroupTest {
+
+    // all tests are 20ms or less, but use a big timeout just in case very slow machine!
+    private static final long TIMEOUT_MS = 15000;
+    
+    private TestApplication app;
+    private SimulatedLocation loc;
+    private Group containerGroup;
+    private ItemsInContainersGroup itemGroup;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        loc = new SimulatedLocation(MutableMap.of("name", "loc"));
+        
+        app = ApplicationBuilder.newManagedApp(TestApplication.class);
+        containerGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
+                .displayName("containerGroup")
+                .configure(DynamicGroup.ENTITY_FILTER, new Predicate<Entity>() {
+                    public boolean apply(Entity input) {
+                        return input instanceof MockContainerEntity && 
+                                input.getConfig(MockContainerEntity.MOCK_MEMBERSHIP) == "ingroup";
+                    }}));
+        itemGroup = app.createAndManageChild(EntitySpec.create(ItemsInContainersGroup.class)
+                .displayName("itemGroup"));
+        itemGroup.setContainers(containerGroup);
+        
+        app.start(ImmutableList.of(loc));
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+
+    @Test
+    public void testSimpleMembership() throws Exception {
+        MockContainerEntity containerIn = newContainer(app, "A", "ingroup");
+        MockItemEntity item1 = newItem(app, containerIn, "1");
+        MockItemEntity item2 = newItem(app, containerIn, "2");
+        
+        assertItemsEventually(item1, item2);
+    }
+
+    @Test
+    public void testFilterIsAppliedToItems() throws Exception {
+        itemGroup.stop();
+        Entities.unmanage(itemGroup);
+        
+        itemGroup = app.createAndManageChild(EntitySpec.create(ItemsInContainersGroup.class)
+                .displayName("itemGroupWithDispName2")
+                .configure(ItemsInContainersGroup.ITEM_FILTER, new Predicate<Entity>() {
+                    public boolean apply(Entity input) {
+                        return "2".equals(input.getDisplayName());
+                    }}));
+        itemGroup.setContainers(containerGroup);
+        
+        MockContainerEntity containerIn = newContainer(app, "A", "ingroup");
+        MockItemEntity item1 = newItem(app, containerIn, "1");
+        MockItemEntity item2 = newItem(app, containerIn, "2");
+        
+        assertItemsEventually(item2); // does not include item1
+    }
+
+    @Test
+    public void testItemsInOtherContainersIgnored() throws Exception {
+        MockContainerEntity containerOut = newContainer(app, "A", "outgroup");
+        MockItemEntity item1 = newItem(app, containerOut, "1");
+        
+        assertItemsEventually();
+    }
+    
+    @Test
+    public void testItemMovedInIsAdded() throws Exception {
+        MockContainerEntity containerIn = newContainer(app, "A", "ingroup");
+        MockContainerEntity containerOut = newContainer(app, "A", "outgroup");
+        MockItemEntity item1 = newItem(app, containerOut, "1");
+        item1.move(containerIn);
+        
+        assertItemsEventually(item1);
+    }
+
+    @Test
+    public void testItemMovedOutIsRemoved() throws Exception {
+        MockContainerEntity containerIn = newContainer(app, "A", "ingroup");
+        MockContainerEntity containerOut = newContainer(app, "A", "outgroup");
+        MockItemEntity item1 = newItem(app, containerIn, "1");
+        assertItemsEventually(item1);
+        
+        item1.move(containerOut);
+        assertItemsEventually();
+    }
+
+    /*
+     * Previously could fail if...
+     * ItemsInContainersGroupImpl listener got notified of Movable.CONTAINER after entity was unmanaged
+     * (because being done in concurrent threads).
+     * This called ItemsInContainersGroupImpl.onItemMoved, which called addMember to add it back in again.
+     * In AbstractGroup.addMember, we now check if the entity is still managed, to 
+     * ensure there is synchronization for concurrent calls to add/remove member.
+     */
+    @Test
+    public void testItemUnmanagedIsRemoved() throws Exception {
+        MockContainerEntity containerIn = newContainer(app, "A", "ingroup");
+        MockItemEntity item1 = newItem(app, containerIn, "1");
+        assertItemsEventually(item1);
+        
+        Entities.unmanage(item1);
+        assertItemsEventually();
+    }
+
+    // TODO How to test this? Will it be used?
+    // Adding a new container then adding items to it is tested in many other methods.
+    @Test(enabled=false)
+    public void testContainerAddedWillAddItsItems() throws Exception {
+    }
+
+    @Test
+    public void testContainerRemovedWillRemoveItsItems() throws Exception {
+        MockContainerEntity containerA = newContainer(app, "A", "ingroup");
+        MockItemEntity item1 = newItem(app, containerA, "1");
+        assertItemsEventually(item1);
+        
+        Entities.unmanage(containerA);
+        assertItemsEventually();
+    }
+
+    private void assertItemsEventually(final MockItemEntity... expected) {
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            public void run() {
+                assertEquals(ImmutableSet.copyOf(itemGroup.getMembers()), ImmutableSet.copyOf(expected));
+            }});
+    }   
+     
+    private MockContainerEntity newContainer(TestApplication app, String name, String membership) {
+        MockContainerEntity container = app.createAndManageChild(EntitySpec.create(MockContainerEntity.class)
+                        .displayName(name)
+                        .configure(MockContainerEntity.MOCK_MEMBERSHIP, membership));
+        container.start(ImmutableList.of(loc));
+        return container;
+    }
+    
+    private static MockItemEntity newItem(TestApplication app, MockContainerEntity container, String name) {
+        MockItemEntity item = app.createAndManageChild(EntitySpec.create(MockItemEntity.class)
+                .displayName(name));
+        item.move(container);
+        return item;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingModelTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingModelTest.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingModelTest.java
new file mode 100644
index 0000000..d96f509
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingModelTest.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Collections;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class LoadBalancingModelTest {
+
+    private static final double PRECISION = 0.00001;
+    
+    private MockContainerEntity container1 = new MockContainerEntityImpl();
+    private MockContainerEntity container2 = new MockContainerEntityImpl();
+    private MockItemEntity item1 = new MockItemEntityImpl();
+    private MockItemEntity item2 = new MockItemEntityImpl();
+    private MockItemEntity item3 = new MockItemEntityImpl();
+    
+    private DefaultBalanceablePoolModel<MockContainerEntity, MockItemEntity> model;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        model = new DefaultBalanceablePoolModel<MockContainerEntity, MockItemEntity>("myname");
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        // nothing to tear down; no management context created
+    }
+
+    @Test
+    public void testPoolRatesCorrectlySumContainers() throws Exception {
+        model.onContainerAdded(container1, 10d, 20d);
+        model.onContainerAdded(container2, 11d, 22d);
+        
+        assertEquals(model.getPoolLowThreshold(), 10d+11d, PRECISION);
+        assertEquals(model.getPoolHighThreshold(), 20d+22d, PRECISION);
+    }
+    
+    @Test
+    public void testPoolRatesCorrectlySumItems() throws Exception {
+        model.onContainerAdded(container1, 10d, 20d);
+        model.onItemAdded(item1, container1, true);
+        model.onItemAdded(item2, container1, true);
+        
+        model.onItemWorkrateUpdated(item1, 1d);
+        assertEquals(model.getCurrentPoolWorkrate(), 1d, PRECISION);
+        
+        model.onItemWorkrateUpdated(item2, 2d);
+        assertEquals(model.getCurrentPoolWorkrate(), 1d+2d, PRECISION);
+        
+        model.onItemWorkrateUpdated(item2, 4d);
+        assertEquals(model.getCurrentPoolWorkrate(), 1d+4d, PRECISION);
+        
+        model.onItemRemoved(item1);
+        assertEquals(model.getCurrentPoolWorkrate(), 4d, PRECISION);
+    }
+    
+    @Test
+    public void testWorkrateUpdateAfterItemRemovalIsNotRecorded() throws Exception {
+        model.onContainerAdded(container1, 10d, 20d);
+        model.onItemAdded(item1, container1, true);
+        model.onItemRemoved(item1);
+        model.onItemWorkrateUpdated(item1, 123d);
+        
+        assertEquals(model.getCurrentPoolWorkrate(), 0d, PRECISION);
+        assertEquals(model.getContainerWorkrates().get(container1), 0d, PRECISION);
+        assertEquals(model.getItemWorkrate(item1), null);
+    }
+    
+    @Test
+    public void testItemMovedWillUpdateContainerWorkrates() throws Exception {
+        model.onContainerAdded(container1, 10d, 20d);
+        model.onContainerAdded(container2, 11d, 21d);
+        model.onItemAdded(item1, container1, false);
+        model.onItemWorkrateUpdated(item1, 123d);
+        
+        model.onItemMoved(item1, container2);
+        
+        assertEquals(model.getItemsForContainer(container1), Collections.emptySet());
+        assertEquals(model.getItemsForContainer(container2), ImmutableSet.of(item1));
+        assertEquals(model.getItemWorkrate(item1), 123d);
+        assertEquals(model.getTotalWorkrate(container1), 0d);
+        assertEquals(model.getTotalWorkrate(container2), 123d);
+        assertEquals(model.getItemWorkrates(container1), Collections.emptyMap());
+        assertEquals(model.getItemWorkrates(container2), ImmutableMap.of(item1, 123d));
+        assertEquals(model.getContainerWorkrates(), ImmutableMap.of(container1, 0d, container2, 123d));
+        assertEquals(model.getCurrentPoolWorkrate(), 123d);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicyConcurrencyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicyConcurrencyTest.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicyConcurrencyTest.java
new file mode 100644
index 0000000..5a3b328
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicyConcurrencyTest.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+
+import com.google.common.collect.Lists;
+
+public class LoadBalancingPolicyConcurrencyTest extends AbstractLoadBalancingPolicyTest {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(LoadBalancingPolicyConcurrencyTest.class);
+
+    private static final double WORKRATE_JITTER = 2d;
+    private static final int NUM_CONTAINERS = 20;
+    private static final int WORKRATE_UPDATE_PERIOD_MS = 1000;
+    
+    private ScheduledExecutorService scheduledExecutor;
+
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void before() {
+        scheduledExecutor = Executors.newScheduledThreadPool(10);
+        super.before();
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    @Override
+    public void after() {
+        if (scheduledExecutor != null) scheduledExecutor.shutdownNow();
+        super.after();
+    }
+    
+    @Test
+    public void testSimplePeriodicWorkrateUpdates() {
+        List<MockItemEntity> items = Lists.newArrayList();
+        List<MockContainerEntity> containers = Lists.newArrayList();
+        
+        for (int i = 0; i < NUM_CONTAINERS; i++) {
+            containers.add(newContainer(app, "container"+i, 10, 30));
+        }
+        for (int i = 0; i < NUM_CONTAINERS; i++) {
+            newItemWithPeriodicWorkrates(app, containers.get(0), "item"+i, 20);
+        }
+
+        assertWorkratesEventually(containers, items, Collections.nCopies(NUM_CONTAINERS, 20d), WORKRATE_JITTER);
+    }
+    
+    @Test
+    public void testConcurrentlyAddContainers() {
+        final Queue<MockContainerEntity> containers = new ConcurrentLinkedQueue<MockContainerEntity>();
+        final List<MockItemEntity> items = Lists.newArrayList();
+        
+        containers.add(newContainer(app, "container-orig", 10, 30));
+        
+        for (int i = 0; i < NUM_CONTAINERS; i++) {
+            items.add(newItemWithPeriodicWorkrates(app, containers.iterator().next(), "item"+i, 20));
+        }
+        for (int i = 0; i < NUM_CONTAINERS-1; i++) {
+            final int index = i;
+            scheduledExecutor.submit(new Callable<Void>() {
+                @Override public Void call() {
+                    containers.add(newContainer(app, "container"+index, 10, 30));
+                    return null;
+                }});
+        }
+
+        assertWorkratesEventually(containers, items, Collections.nCopies(NUM_CONTAINERS, 20d), WORKRATE_JITTER);
+    }
+    
+    @Test
+    public void testConcurrentlyAddItems() {
+        final Queue<MockItemEntity> items = new ConcurrentLinkedQueue<MockItemEntity>();
+        final List<MockContainerEntity> containers = Lists.newArrayList();
+        
+        for (int i = 0; i < NUM_CONTAINERS; i++) {
+            containers.add(newContainer(app, "container"+i, 10, 30));
+        }
+        for (int i = 0; i < NUM_CONTAINERS; i++) {
+            final int index = i;
+            scheduledExecutor.submit(new Callable<Void>() {
+                @Override public Void call() {
+                    items.add(newItemWithPeriodicWorkrates(app, containers.get(0), "item"+index, 20));
+                    return null;
+                }});
+        }
+        assertWorkratesEventually(containers, items, Collections.nCopies(NUM_CONTAINERS, 20d), WORKRATE_JITTER);
+    }
+    
+    // TODO Got IndexOutOfBoundsException from containers.last()
+    @Test(groups="WIP", invocationCount=100)
+    public void testConcurrentlyRemoveContainers() {
+        List<MockItemEntity> items = Lists.newArrayList();
+        final List<MockContainerEntity> containers = Lists.newArrayList();
+        
+        for (int i = 0; i < NUM_CONTAINERS; i++) {
+            containers.add(newContainer(app, "container"+i, 15, 45));
+        }
+        for (int i = 0; i < NUM_CONTAINERS; i++) {
+            items.add(newItemWithPeriodicWorkrates(app, containers.get(i), "item"+i, 20));
+        }
+        
+        final List<MockContainerEntity> containersToStop = Lists.newArrayList();
+        for (int i = 0; i < NUM_CONTAINERS/2; i++) {
+            containersToStop.add(containers.remove(0));
+        }
+        for (final MockContainerEntity containerToStop : containersToStop) {
+            scheduledExecutor.submit(new Callable<Void>() {
+                @Override public Void call() {
+                    try {
+                        containerToStop.offloadAndStop(containers.get(containers.size()-1));
+                        Entities.unmanage(containerToStop);
+                    } catch (Throwable t) {
+                        LOG.error("Error stopping container "+containerToStop, t);
+                    }
+                    return null;
+                }});
+        }
+        
+        assertWorkratesEventually(containers, items, Collections.nCopies((int)(NUM_CONTAINERS/2), 40d), WORKRATE_JITTER*2);
+    }
+    
+    @Test(groups="WIP")
+    public void testConcurrentlyRemoveItems() {
+        List<MockItemEntity> items = Lists.newArrayList();
+        List<MockContainerEntity> containers = Lists.newArrayList();
+        
+        for (int i = 0; i < NUM_CONTAINERS; i++) {
+            containers.add(newContainer(app, "container"+i, 15, 45));
+        }
+        for (int i = 0; i < NUM_CONTAINERS*2; i++) {
+            items.add(newItemWithPeriodicWorkrates(app, containers.get(i%NUM_CONTAINERS), "item"+i, 20));
+        }
+        // should now have item0 and item{0+NUM_CONTAINERS} on container0, etc
+        
+        for (int i = 0; i < NUM_CONTAINERS; i++) {
+            // not removing consecutive items as that would leave it balanced!
+            int indexToStop = (i < NUM_CONTAINERS/2) ? NUM_CONTAINERS : 0; 
+            final MockItemEntity itemToStop = items.remove(indexToStop);
+            scheduledExecutor.submit(new Callable<Void>() {
+                @Override public Void call() {
+                    try {
+                        itemToStop.stop();
+                        Entities.unmanage(itemToStop);
+                    } catch (Throwable t) {
+                        LOG.error("Error stopping item "+itemToStop, t);
+                    }
+                    return null;
+                }});
+        }
+        
+        assertWorkratesEventually(containers, items, Collections.nCopies(NUM_CONTAINERS, 20d), WORKRATE_JITTER);
+    }
+    
+    protected MockItemEntity newItemWithPeriodicWorkrates(TestApplication app, MockContainerEntity container, String name, double workrate) {
+        MockItemEntity item = newItem(app, container, name, workrate);
+        scheduleItemWorkrateUpdates(item, workrate, WORKRATE_JITTER);
+        return item;
+    }
+    
+    private void scheduleItemWorkrateUpdates(final MockItemEntity item, final double workrate, final double jitter) {
+        final AtomicReference<Future<?>> futureRef = new AtomicReference<Future<?>>();
+        Future<?> future = scheduledExecutor.scheduleAtFixedRate(
+                new Runnable() {
+                    @Override public void run() {
+                        if (item.isStopped() && futureRef.get() != null) {
+                            futureRef.get().cancel(true);
+                            return;
+                        }
+                        double jitteredWorkrate = workrate + (random.nextDouble()*jitter*2 - jitter);
+                        ((EntityLocal)item).setAttribute(TEST_METRIC, (int) Math.max(0, jitteredWorkrate));
+                    }
+                },
+                0, WORKRATE_UPDATE_PERIOD_MS, TimeUnit.MILLISECONDS);
+        futureRef.set(future);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicySoakTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicySoakTest.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicySoakTest.java
new file mode 100644
index 0000000..8da494c
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicySoakTest.java
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+public class LoadBalancingPolicySoakTest extends AbstractLoadBalancingPolicyTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(LoadBalancingPolicySoakTest.class);
+    
+    private static final long TIMEOUT_MS = 40*1000;
+    
+    @Test
+    public void testLoadBalancingQuickTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 1;
+        config.numContainers = 5;
+        config.numItems = 5;
+        config.lowThreshold = 200;
+        config.highThreshold = 300;
+        config.totalRate = (int) (config.numContainers*(0.95*config.highThreshold));
+    
+        runLoadBalancingSoakTest(config);
+    }
+    
+    @Test
+    public void testLoadBalancingManyItemsQuickTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 1;
+        config.numContainers = 5;
+        config.numItems = 30;
+        config.lowThreshold = 200;
+        config.highThreshold = 300;
+        config.numContainerStopsPerCycle = 1;
+        config.numItemStopsPerCycle = 1;
+        config.totalRate = (int) (config.numContainers*(0.95*config.highThreshold));
+    
+        runLoadBalancingSoakTest(config);
+    }
+    
+    @Test(groups={"Integration","Acceptance"}) // acceptance group, because it's slow to run many cycles
+    public void testLoadBalancingSoakTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 100;
+        config.numContainers = 5;
+        config.numItems = 5;
+        config.lowThreshold = 200;
+        config.highThreshold = 300;
+        config.totalRate = (int) (config.numContainers*(0.95*config.highThreshold));
+    
+        runLoadBalancingSoakTest(config);
+    }
+
+    @Test(groups={"Integration","Acceptance"}) // acceptance group, because it's slow to run many cycles
+    public void testLoadBalancingManyItemsSoakTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 100;
+        config.numContainers = 5;
+        config.numItems = 30;
+        config.lowThreshold = 200;
+        config.highThreshold = 300;
+        config.totalRate = (int) (config.numContainers*(0.95*config.highThreshold));
+        config.numContainerStopsPerCycle = 3;
+        config.numItemStopsPerCycle = 10;
+        
+        runLoadBalancingSoakTest(config);
+    }
+
+    @Test(groups={"Integration","Acceptance"})
+    public void testLoadBalancingManyManyItemsTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 1;
+        config.numContainers = 5;
+        config.numItems = 1000;
+        config.lowThreshold = 2000;
+        config.highThreshold = 3000;
+        config.numContainerStopsPerCycle = 0;
+        config.numItemStopsPerCycle = 0;
+        config.totalRate = (int) (config.numContainers*(0.95*config.highThreshold));
+        config.verbose = false;
+        
+        runLoadBalancingSoakTest(config);
+    }
+    
+    private void runLoadBalancingSoakTest(RunConfig config) {
+        final int numCycles = config.numCycles;
+        final int numContainers = config.numContainers;
+        final int numItems = config.numItems;
+        final double lowThreshold = config.lowThreshold;
+        final double highThreshold = config.highThreshold;
+        final int totalRate = config.totalRate;
+        final int numContainerStopsPerCycle = config.numContainerStopsPerCycle;
+        final int numItemStopsPerCycle = config.numItemStopsPerCycle;
+        final boolean verbose = config.verbose;
+        
+        MockItemEntityImpl.totalMoveCount.set(0);
+        
+        final List<MockContainerEntity> containers = new ArrayList<MockContainerEntity>();
+        final List<MockItemEntity> items = new ArrayList<MockItemEntity>();
+
+        for (int i = 1; i <= numContainers; i++) {
+            MockContainerEntity container = newContainer(app, "container-"+i, lowThreshold, highThreshold);
+            containers.add(container);
+        }
+        for (int i = 1; i <= numItems; i++) {
+            MockItemEntity item = newItem(app, containers.get(0), "item-"+i, 5);
+            items.add(item);
+        }
+        
+        for (int i = 1; i <= numCycles; i++) {
+            LOG.info(LoadBalancingPolicySoakTest.class.getSimpleName()+": cycle "+i);
+            
+            // Stop items, and start others
+            for (int j = 1; j <= numItemStopsPerCycle; j++) {
+                int itemIndex = random.nextInt(numItems);
+                MockItemEntity itemToStop = items.get(itemIndex);
+                itemToStop.stop();
+                LOG.debug("Unmanaging item {}", itemToStop);
+                Entities.unmanage(itemToStop);
+                items.set(itemIndex, newItem(app, containers.get(0), "item-"+(itemIndex+1)+"."+i+"."+j, 5));
+            }
+            
+            // Repartition the load across the items
+            final List<Integer> itemRates = randomlyDivideLoad(numItems, totalRate, 0, (int)highThreshold);
+            
+            for (int j = 0; j < numItems; j++) {
+                MockItemEntity item = items.get(j);
+                ((EntityLocal)item).setAttribute(MockItemEntity.TEST_METRIC, itemRates.get(j));
+            }
+                
+            // Stop containers, and start others
+            for (int j = 1; j <= numContainerStopsPerCycle; j++) {
+                int containerIndex = random.nextInt(numContainers);
+                MockContainerEntity containerToStop = containers.get(containerIndex);
+                containerToStop.offloadAndStop(containers.get((containerIndex+1)%numContainers));
+                LOG.debug("Unmanaging container {}", containerToStop);
+                Entities.unmanage(containerToStop);
+                
+                MockContainerEntity containerToAdd = newContainer(app, "container-"+(containerIndex+1)+"."+i+"."+j, lowThreshold, highThreshold);
+                containers.set(containerIndex, containerToAdd);
+            }
+
+            // Assert that the items become balanced again
+            Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+                @Override public void run() {
+                    Iterable<Double> containerRates = Iterables.transform(containers, new Function<MockContainerEntity, Double>() {
+                        @Override public Double apply(MockContainerEntity input) {
+                            return (double) input.getWorkrate();
+                        }});
+                    
+                    String errMsg;
+                    if (verbose) {
+                        errMsg = verboseDumpToString(containers, items)+"; itemRates="+itemRates;
+                    } else {
+                        errMsg = containerRates+"; totalMoves="+MockItemEntityImpl.totalMoveCount;
+                    }
+                    
+                    // Check that haven't lost any items
+                    // (as observed in one jenkins build failure: 2014-03-18; but that could also be 
+                    // explained by errMsg generated in the middle of a move)
+                    List<Entity> itemsFromModel = Lists.newArrayList();
+                    List<Entity> itemsFromContainers = Lists.newArrayList();
+                    for (Entity container : model.getPoolContents()) {
+                        itemsFromModel.addAll(model.getItemsForContainer(container));
+                    }
+                    for (MockContainerEntity container : containers) {
+                        itemsFromContainers.addAll(container.getBalanceableItems());
+                    }
+                    Asserts.assertEqualsIgnoringOrder(itemsFromModel, items, true, errMsg);
+                    Asserts.assertEqualsIgnoringOrder(itemsFromContainers, items, true, errMsg);
+                    
+                    // Check overall container rates are balanced
+                    assertEquals(sum(containerRates), sum(itemRates), errMsg);
+                    for (double containerRate : containerRates) {
+                        assertTrue(containerRate >= lowThreshold, errMsg);
+                        assertTrue(containerRate <= highThreshold, errMsg);
+                    }
+                }});
+        }
+    }
+    
+    private static class RunConfig {
+        int numCycles = 1;
+        int numContainers = 5;
+        int numItems = 5;
+        double lowThreshold = 200;
+        double highThreshold = 300;
+        int totalRate = (int) (5*(0.95*highThreshold));
+        int numContainerStopsPerCycle = 1;
+        int numItemStopsPerCycle = 1;
+        boolean verbose = true;
+    }
+
+    // Testing conveniences.
+    
+    private double sum(Iterable<? extends Number> vals) {
+        double total = 0;;
+        for (Number val : vals) {
+            total += val.doubleValue();
+        }
+        return total;
+    }
+    
+    /**
+     * Distributes a given load across a number of items randomly. The variability in load for an item is dictated by the variance,
+     * but the total will always equal totalLoad.
+     * 
+     * The distribution of load is skewed: one side of the list will have bigger values than the other.
+     * Which side is skewed will vary, so when balancing a policy will find that things have entirely changed.
+     * 
+     * TODO This is not particularly good at distributing load, but it's random and skewed enough to force rebalancing.
+     */
+    private List<Integer> randomlyDivideLoad(int numItems, int totalLoad, int min, int max) {
+        List<Integer> result = new ArrayList<Integer>(numItems);
+        int totalRemaining = totalLoad;
+        int variance = 3;
+        int skew = 3;
+        
+        for (int i = 0; i < numItems; i++) {
+            int itemsRemaining = numItems-i;
+            int itemFairShare = (totalRemaining/itemsRemaining);
+            double skewFactor = ((double)i/numItems)*2 - 1; // a number between -1 and 1, depending how far through the item set we are
+            int itemSkew = (int) (random.nextInt(skew)*skewFactor);
+            int itemLoad = itemFairShare + (random.nextInt(variance*2)-variance) + itemSkew;
+            itemLoad = Math.max(min, itemLoad);
+            itemLoad = Math.min(totalRemaining, itemLoad);
+            itemLoad = Math.min(max, itemLoad);
+            result.add(itemLoad);
+            totalRemaining -= itemLoad;
+        }
+
+        if (random.nextBoolean()) Collections.reverse(result);
+        
+        assertTrue(sum(result) <= totalLoad, "totalLoad="+totalLoad+"; result="+result);
+        
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicyTest.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicyTest.java
new file mode 100644
index 0000000..a0166f9
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicyTest.java
@@ -0,0 +1,397 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class LoadBalancingPolicyTest extends AbstractLoadBalancingPolicyTest {
+    
+    // Expect no balancing to occur as container A isn't above the high threshold.
+    @Test
+    public void testNoopWhenWithinThresholds() {
+        MockContainerEntity containerA = newContainer(app, "A", 10, 100);
+        MockContainerEntity containerB = newContainer(app, "B", 20, 60);
+        MockItemEntity item1 = newItem(app, containerA, "1", 10);
+        MockItemEntity item2 = newItem(app, containerA, "2", 10);
+        MockItemEntity item3 = newItem(app, containerA, "3", 10);
+        MockItemEntity item4 = newItem(app, containerA, "4", 10);
+
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3, item4), 
+                ImmutableList.of(40d, 0d));
+    }
+    
+    @Test
+    public void testNoopWhenAlreadyBalanced() {
+        MockContainerEntity containerA = newContainer(app, "A", 20, 80);
+        MockContainerEntity containerB = newContainer(app, "B", 20, 80);
+        MockItemEntity item1 = newItem(app, containerA, "1", 10);
+        MockItemEntity item2 = newItem(app, containerA, "2", 30);
+        MockItemEntity item3 = newItem(app, containerB, "3", 20);
+        MockItemEntity item4 = newItem(app, containerB, "4", 20);
+
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3, item4), 
+                ImmutableList.of(40d, 40d));
+        assertEquals(containerA.getBalanceableItems(), ImmutableSet.of(item1, item2));
+        assertEquals(containerB.getBalanceableItems(), ImmutableSet.of(item3, item4));
+    }
+    
+    // Expect 20 units of workload to be migrated from hot container (A) to cold (B).
+    @Test
+    public void testSimpleBalancing() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 25);
+        MockContainerEntity containerB = newContainer(app, "B", 20, 60);
+        MockItemEntity item1 = newItem(app, containerA, "1", 10);
+        MockItemEntity item2 = newItem(app, containerA, "2", 10);
+        MockItemEntity item3 = newItem(app, containerA, "3", 10);
+        MockItemEntity item4 = newItem(app, containerA, "4", 10);
+
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3, item4), 
+                ImmutableList.of(20d, 20d));
+    }
+    
+    @Test
+    public void testSimpleBalancing2() {
+        MockContainerEntity containerA = newContainer(app, "A", 20, 40);
+        MockContainerEntity containerB = newContainer(app, "B", 20, 40);
+        MockItemEntity item1 = newItem(app, containerA, "1", 0);
+        MockItemEntity item2 = newItem(app, containerB, "2", 40);
+        MockItemEntity item3 = newItem(app, containerB, "3", 20);
+        MockItemEntity item4 = newItem(app, containerB, "4", 20);
+
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3, item4), 
+                ImmutableList.of(40d, 40d));
+    }
+    
+//    @Test
+//    public void testAdjustedItemNotMoved() {
+//        MockBalancingModel pool = new MockBalancingModel(
+//                containers(
+//                        containerA, 20, 50,
+//                        containerB, 20, 50),
+//                items(
+//                        "item1", containerA, 0,
+//                        "item2", containerB, -40,
+//                        "item3", containerB, 20,
+//                        "item4", containerB, 20)
+//        );
+//        
+//        BalancingStrategy<String, String> policy = new BalancingStrategy<String, String>("Test", pool);
+//        policy.rebalance();
+//        
+//        assertEquals((Object)pool.getItemsForContainer(containerA), ImmutableSet.of("item1", "item3", "item4"), pool.itemDistributionToString());
+//        assertEquals((Object)pool.getItemsForContainer(containerB), ImmutableSet.of("item2"), pool.itemDistributionToString());
+//    }
+
+    @Test
+    public void testMultiMoveBalancing() {
+        MockContainerEntity containerA = newContainer(app, "A", 20, 50);
+        MockContainerEntity containerB = newContainer(app, "B", 20, 50);
+        MockItemEntity item1 = newItem(app, containerA, "1", 10);
+        MockItemEntity item2 = newItem(app, containerA, "2", 10);
+        MockItemEntity item3 = newItem(app, containerA, "3", 10);
+        MockItemEntity item4 = newItem(app, containerA, "4", 10);
+        MockItemEntity item5 = newItem(app, containerA, "5", 10);
+        MockItemEntity item6 = newItem(app, containerA, "6", 10);
+        MockItemEntity item7 = newItem(app, containerA, "7", 10);
+        MockItemEntity item8 = newItem(app, containerA, "8", 10);
+        MockItemEntity item9 = newItem(app, containerA, "9", 10);
+        MockItemEntity item10 = newItem(app, containerA, "10", 10);
+
+        // non-deterministic which items will be moved; but can assert how many (given they all have same workrate)
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3, item4, item5, item6, item7, item8, item9, item10), 
+                ImmutableList.of(50d, 50d));
+        assertEquals(containerA.getBalanceableItems().size(), 5);
+        assertEquals(containerB.getBalanceableItems().size(), 5);
+    }
+    
+    @Test
+    public void testRebalanceWhenWorkratesChange() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
+        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
+        MockItemEntity item1 = newItem(app, containerA, "1", 0);
+        MockItemEntity item2 = newItem(app, containerA, "2", 0);
+
+        ((EntityLocal)item1).setAttribute(MockItemEntity.TEST_METRIC, 40);
+        ((EntityLocal)item2).setAttribute(MockItemEntity.TEST_METRIC, 40);
+        
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2), 
+                ImmutableList.of(40d, 40d));
+    }
+    
+    // Expect no balancing to occur in hot pool (2 containers over-threshold at 40).
+    // On addition of new container, expect hot containers to offload 10 each.
+    @Test
+    public void testAddContainerWhenHot() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 30);
+        MockContainerEntity containerB = newContainer(app, "B", 10, 30);
+        MockItemEntity item1 = newItem(app, containerA, "1", 10);
+        MockItemEntity item2 = newItem(app, containerA, "2", 10);
+        MockItemEntity item3 = newItem(app, containerA, "3", 10);
+        MockItemEntity item4 = newItem(app, containerA, "4", 10);
+        MockItemEntity item5 = newItem(app, containerB, "5", 10);
+        MockItemEntity item6 = newItem(app, containerB, "6", 10);
+        MockItemEntity item7 = newItem(app, containerB, "7", 10);
+        MockItemEntity item8 = newItem(app, containerB, "8", 10);
+        // Both containers are over-threshold at this point; should not rebalance.
+        
+        MockContainerEntity containerC = newAsyncContainer(app, "C", 10, 30, CONTAINER_STARTUP_DELAY_MS);
+        // New container allows hot ones to offload work.
+
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB, containerC), 
+                ImmutableList.of(item1, item2, item3, item4, item5, item6, item7, item8), 
+                ImmutableList.of(30d, 30d, 20d));
+    }
+
+    // On addition of new container, expect no rebalancing to occur as no existing container is hot.
+    @Test
+    public void testAddContainerWhenCold() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
+        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
+        MockItemEntity item1 = newItem(app, containerA, "1", 10);
+        MockItemEntity item2 = newItem(app, containerA, "2", 10);
+        MockItemEntity item3 = newItem(app, containerA, "3", 10);
+        MockItemEntity item4 = newItem(app, containerA, "4", 10);
+        MockItemEntity item5 = newItem(app, containerB, "5", 10);
+        MockItemEntity item6 = newItem(app, containerB, "6", 10);
+        MockItemEntity item7 = newItem(app, containerB, "7", 10);
+        MockItemEntity item8 = newItem(app, containerB, "8", 10);
+        
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3, item4, item5, item6, item7, item8), 
+                ImmutableList.of(40d, 40d));
+        
+        MockContainerEntity containerC = newAsyncContainer(app, "C", 10, 50, CONTAINER_STARTUP_DELAY_MS);
+
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB, containerC), 
+                ImmutableList.of(item1, item2, item3, item4, item5, item6, item7, item8), 
+                ImmutableList.of(40d, 40d, 0d));
+    }
+    
+    // Expect no balancing to occur in cool pool (2 containers under-threshold at 30).
+    // On addition of new item, expect over-threshold container (A) to offload 20 to B.
+    @Test
+    public void testAddItem() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
+        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
+        MockItemEntity item1 = newItem(app, containerA, "1", 10);
+        MockItemEntity item2 = newItem(app, containerA, "2", 10);
+        MockItemEntity item3 = newItem(app, containerA, "3", 10);
+        MockItemEntity item4 = newItem(app, containerB, "4", 10);
+        MockItemEntity item5 = newItem(app, containerB, "5", 10);
+        MockItemEntity item6 = newItem(app, containerB, "6", 10);
+        
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3, item4, item5, item6), 
+                ImmutableList.of(30d, 30d));
+        
+        newItem(app, containerA, "7", 40);
+        
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3, item4, item5, item6), 
+                ImmutableList.of(50d, 50d));
+    }
+    
+    // FIXME Failed in build repeatedly (e.g. #1035), but couldn't reproduce locally yet with invocationCount=100
+    @Test(groups="WIP")
+    public void testRemoveContainerCausesRebalancing() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 30);
+        MockContainerEntity containerB = newContainer(app, "B", 10, 30);
+        MockContainerEntity containerC = newContainer(app, "C", 10, 30);
+        MockItemEntity item1 = newItem(app, containerA, "1", 10);
+        MockItemEntity item2 = newItem(app, containerA, "2", 10);
+        MockItemEntity item3 = newItem(app, containerB, "3", 10);
+        MockItemEntity item4 = newItem(app, containerB, "4", 10);
+        MockItemEntity item5 = newItem(app, containerC, "5", 10);
+        MockItemEntity item6 = newItem(app, containerC, "6", 10);
+
+        Entities.unmanage(containerC);
+        item5.move(containerA);
+        item6.move(containerA);
+        
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3, item4, item5, item6), 
+                ImmutableList.of(30d, 30d));
+    }
+
+    @Test
+    public void testRemoveItemCausesRebalancing() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 30);
+        MockContainerEntity containerB = newContainer(app, "B", 10, 30);
+        MockItemEntity item1 = newItem(app, containerA, "1", 30);
+        MockItemEntity item2 = newItem(app, containerB, "2", 20);
+        MockItemEntity item3 = newItem(app, containerB, "3", 20);
+        
+        item1.stop();
+        Entities.unmanage(item1);
+        
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3), 
+                ImmutableList.of(20d, 20d));
+    }
+
+    @Test
+    public void testRebalancesAfterManualMove() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
+        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
+        MockItemEntity item1 = newItem(app, containerA, "1", 20);
+        MockItemEntity item2 = newItem(app, containerA, "2", 20);
+        MockItemEntity item3 = newItem(app, containerB, "3", 20);
+        MockItemEntity item4 = newItem(app, containerB, "4", 20);
+
+        // Move everything onto containerA, and expect it to be automatically re-balanced
+        item3.move(containerA);
+        item4.move(containerA);
+
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3, item4), 
+                ImmutableList.of(40d, 40d));
+    }
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @Test
+    public void testModelIncludesItemsAndContainersStartedBeforePolicyCreated() {
+        pool.removePolicy(policy);
+        policy.destroy();
+        
+        // Set-up containers and items.
+        final MockContainerEntity containerA = newContainer(app, "A", 10, 100);
+        newItem(app, containerA, "1", 10);
+
+        policy = new LoadBalancingPolicy(MutableMap.of(), TEST_METRIC, model);
+        pool.addPolicy(policy);
+        
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            public void run() {
+                assertEquals(model.getContainerWorkrates(), ImmutableMap.of(containerA, 10d));
+            }
+        });
+    }
+    
+    @Test
+    public void testLockedItemsNotMoved() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
+        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
+        MockItemEntity item1 = newLockedItem(app, containerA, "1", 40);
+        MockItemEntity item2 = newLockedItem(app, containerA, "2", 40);
+
+        assertWorkratesContinually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2), 
+                ImmutableList.of(80d, 0d));
+    }
+    
+    @Test
+    public void testLockedItemsContributeToOverloadedMeasurements() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
+        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
+        MockItemEntity item1 = newLockedItem(app, containerA, "1", 40);
+        MockItemEntity item2 = newItem(app, containerA, "2", 25);
+        MockItemEntity item3 = newItem(app, containerA, "3", 25);
+        
+        assertWorkratesEventually(
+                ImmutableList.of(containerA, containerB),
+                ImmutableList.of(item1, item2, item3), 
+                ImmutableList.of(40d, 50d));
+    }
+    
+    @Test
+    public void testOverloadedLockedItemsPreventMoreWorkEnteringContainer() throws Exception {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, "A", 10, 50);
+        MockContainerEntity containerB = newContainer(app, "B", 10, 50);
+        MockItemEntity item1 = newLockedItem(app, containerA, "1", 50);
+        Thread.sleep(1); // increase chances of item1's workrate having been received first
+        MockItemEntity item2 = newItem(app, containerB, "2", 30);
+        MockItemEntity item3 = newItem(app, containerB, "3", 30);
+        
+        assertWorkratesContinually(
+                ImmutableList.of(containerA, containerB), 
+                ImmutableList.of(item1, item2, item3), 
+                ImmutableList.of(50d, 60d));
+    }
+    
+    @Test
+    public void testPolicyUpdatesModel() {
+        final MockContainerEntity containerA = newContainer(app, "A", 10, 20);
+        final MockContainerEntity containerB = newContainer(app, "B", 11, 21);
+        final MockItemEntity item1 = newItem(app, containerA, "1", 12);
+        final MockItemEntity item2 = newItem(app, containerB, "2", 13);
+        
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            public void run() {
+                assertEquals(model.getPoolSize(), 2);
+                assertEquals(model.getPoolContents(), ImmutableSet.of(containerA, containerB));
+                assertEquals(model.getItemWorkrate(item1), 12d);
+                assertEquals(model.getItemWorkrate(item2), 13d);
+                
+                assertEquals(model.getParentContainer(item1), containerA);
+                assertEquals(model.getParentContainer(item2), containerB);
+                assertEquals(model.getContainerWorkrates(), ImmutableMap.of(containerA, 12d, containerB, 13d));
+                
+                assertEquals(model.getPoolLowThreshold(), 10+11d);
+                assertEquals(model.getPoolHighThreshold(), 20+21d);
+                assertEquals(model.getCurrentPoolWorkrate(), 12+13d);
+                assertFalse(model.isHot());
+                assertFalse(model.isCold());
+            }
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockContainerEntity.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockContainerEntity.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockContainerEntity.java
new file mode 100644
index 0000000..1937d9a
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockContainerEntity.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Effector;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.annotation.EffectorParam;
+import brooklyn.entity.basic.AbstractGroup;
+import brooklyn.entity.basic.MethodEffector;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.basic.BasicConfigKey;
+
+@ImplementedBy(MockContainerEntityImpl.class)
+public interface MockContainerEntity extends AbstractGroup, BalanceableContainer<Movable>, Startable {
+
+    @SetFromFlag("membership")
+    public static final ConfigKey<String> MOCK_MEMBERSHIP = new BasicConfigKey<String>(
+            String.class, "mock.container.membership", "For testing ItemsInContainersGroup");
+
+    @SetFromFlag("delay")
+    public static final ConfigKey<Long> DELAY = new BasicConfigKey<Long>(
+            Long.class, "mock.container.delay", "", 0L);
+
+    public static final Effector<Void> OFFLOAD_AND_STOP = new MethodEffector<Void>(MockContainerEntity.class, "offloadAndStop");
+
+    public void lock();
+
+    public void unlock();
+
+    public int getWorkrate();
+
+    public Map<Entity, Double> getItemUsage();
+
+    public void addItem(Entity item);
+
+    public void removeItem(Entity item);
+
+    public void offloadAndStop(@EffectorParam(name="otherContianer") MockContainerEntity otherContainer);
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockContainerEntityImpl.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockContainerEntityImpl.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockContainerEntityImpl.java
new file mode 100644
index 0000000..de9da37
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockContainerEntityImpl.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.event.AttributeSensor;
+import org.apache.brooklyn.api.location.Location;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.AbstractGroupImpl;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+
+public class MockContainerEntityImpl extends AbstractGroupImpl implements MockContainerEntity {
+
+    private static final Logger LOG = LoggerFactory.getLogger(MockContainerEntity.class);
+
+    volatile boolean offloading;
+    volatile boolean running;
+
+    ReentrantLock _lock = new ReentrantLock();
+
+    @Override
+    public <T> T setAttribute(AttributeSensor<T> attribute, T val) {
+        if (LOG.isDebugEnabled()) LOG.debug("Mocks: container {} setting {} to {}", new Object[] {this, attribute, val});
+        return super.setAttribute(attribute, val);
+    }
+
+    @Override
+    public void lock() {
+        _lock.lock();
+        if (!running) {
+            _lock.unlock();
+            throw new IllegalStateException("Container lock "+this+"; it is not running");
+        }
+    }
+
+    @Override
+    public void unlock() {
+        _lock.unlock();
+    }
+
+    @Override
+    public int getWorkrate() {
+        int result = 0;
+        for (Entity member : getMembers()) {
+            Integer memberMetric = member.getAttribute(MockItemEntity.TEST_METRIC);
+            result += ((memberMetric != null) ? memberMetric : 0);
+        }
+        return result;
+    }
+
+    @Override
+    public Map<Entity, Double> getItemUsage() {
+        Map<Entity, Double> result = Maps.newLinkedHashMap();
+        for (Entity member : getMembers()) {
+            Map<Entity, Double> memberItemUsage = member.getAttribute(MockItemEntity.ITEM_USAGE_METRIC);
+            if (memberItemUsage != null) {
+                for (Map.Entry<Entity, Double> entry : memberItemUsage.entrySet()) {
+                    double val = (result.containsKey(entry.getKey()) ? result.get(entry.getKey()) : 0d);
+                    val += ((entry.getValue() != null) ? entry.getValue() : 0);
+                    result.put(entry.getKey(), val);
+                }
+            }
+        }
+        return result;
+    }
+    
+    @Override
+    public void addItem(Entity item) {
+        if (LOG.isDebugEnabled()) LOG.debug("Mocks: adding item {} to container {}", item, this);
+        if (!running || offloading) throw new IllegalStateException("Container "+getDisplayName()+" is not running; cannot add item "+item);
+        addMember(item);
+        emit(ITEM_ADDED, item);
+    }
+
+    @Override
+    public void removeItem(Entity item) {
+        if (LOG.isDebugEnabled()) LOG.debug("Mocks: removing item {} from container {}", item, this);
+        if (!running) throw new IllegalStateException("Container "+getDisplayName()+" is not running; cannot remove item "+item);
+        removeMember(item);
+        emit(ITEM_REMOVED, item);
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @Override
+    public Set<Movable> getBalanceableItems() {
+        return (Set) Sets.newLinkedHashSet(getMembers());
+    }
+
+    public String toString() {
+        return "MockContainer["+getDisplayName()+"]";
+    }
+
+    private long getDelay() {
+        return getConfig(DELAY);
+    }
+    
+    @Override
+    public void start(Collection<? extends Location> locs) {
+        if (LOG.isDebugEnabled()) LOG.debug("Mocks: starting container {}", this);
+        _lock.lock();
+        try {
+            Time.sleep(getDelay());
+            running = true;
+            addLocations(locs);
+            emit(Attributes.LOCATION_CHANGED, null);
+            setAttribute(SERVICE_UP, true);
+        } finally {
+            _lock.unlock();
+        }
+    }
+
+    @Override
+    public void stop() {
+        if (LOG.isDebugEnabled()) LOG.debug("Mocks: stopping container {}", this);
+        _lock.lock();
+        try {
+            running = false;
+            Time.sleep(getDelay());
+            setAttribute(SERVICE_UP, false);
+        } finally {
+            _lock.unlock();
+        }
+    }
+
+    private void stopWithoutLock() {
+        running = false;
+        Time.sleep(getDelay());
+        setAttribute(SERVICE_UP, false);
+    }
+
+    public void offloadAndStop(final MockContainerEntity otherContainer) {
+        if (LOG.isDebugEnabled()) LOG.debug("Mocks: offloading container {} to {} (items {})", new Object[] {this, otherContainer, getBalanceableItems()});
+        runWithLock(ImmutableList.of(this, otherContainer), new Runnable() {
+            public void run() {
+                offloading = false;
+                for (Movable item : getBalanceableItems()) {
+                    ((MockItemEntity)item).moveNonEffector(otherContainer);
+                }
+                if (LOG.isDebugEnabled()) LOG.debug("Mocks: stopping offloaded container {}", this);
+                stopWithoutLock();
+            }});
+    }
+
+    @Override
+    public void restart() {
+        if (LOG.isDebugEnabled()) LOG.debug("Mocks: restarting {}", this);
+        throw new UnsupportedOperationException();
+    }
+
+    public static void runWithLock(List<MockContainerEntity> entitiesToLock, Runnable r) {
+        List<MockContainerEntity> entitiesToLockCopy = MutableList.copyOf(Iterables.filter(entitiesToLock, Predicates.notNull()));
+        List<MockContainerEntity> entitiesLocked = Lists.newArrayList();
+        Collections.sort(entitiesToLockCopy, new Comparator<MockContainerEntity>() {
+            public int compare(MockContainerEntity o1, MockContainerEntity o2) {
+                return o1.getId().compareTo(o2.getId());
+            }});
+
+        try {
+            for (MockContainerEntity it : entitiesToLockCopy) {
+                it.lock();
+                entitiesLocked.add(it);
+            }
+
+            r.run();
+
+        } finally {
+            for (MockContainerEntity it : entitiesLocked) {
+                it.unlock();
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockItemEntity.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockItemEntity.java b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockItemEntity.java
new file mode 100644
index 0000000..83a54c0
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/loadbalancing/MockItemEntity.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
+import org.apache.brooklyn.api.event.AttributeSensor;
+
+import brooklyn.event.basic.Sensors;
+
+import com.google.common.reflect.TypeToken;
+
+@ImplementedBy(MockItemEntityImpl.class)
+public interface MockItemEntity extends Entity, Movable {
+
+    public static final AttributeSensor<Integer> TEST_METRIC = Sensors.newIntegerSensor(
+            "test.metric", "Dummy workrate for test entities");
+
+    @SuppressWarnings("serial")
+    public static final AttributeSensor<Map<Entity, Double>> ITEM_USAGE_METRIC = Sensors.newSensor(
+            new TypeToken<Map<Entity, Double>>() {}, "test.itemUsage.metric", "Dummy item usage for test entities");
+
+    public boolean isStopped();
+
+    public void moveNonEffector(Entity rawDestination);
+    
+    public void stop();
+}



[17/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/BalancingStrategy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/BalancingStrategy.java b/policy/src/main/java/brooklyn/policy/loadbalancing/BalancingStrategy.java
deleted file mode 100644
index 714b70c..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/BalancingStrategy.java
+++ /dev/null
@@ -1,622 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import java.text.MessageFormat;
-import java.util.Collection;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.location.Location;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Represents an abstract algorithm for optimally balancing worker "items" among several "containers" based on the workloads
- * of the items, and corresponding high- and low-thresholds on the containers.
- * 
- * TODO: extract interface, provide default implementation
- * TODO: remove legacy code comments
- */
-public class BalancingStrategy<NodeType extends Entity, ItemType extends Movable> {
-
-    // FIXME Bad idea to use MessageFormat.format in this way; if toString of entity contains
-    // special characters interpreted by MessageFormat, then it will all break!
-    
-    // This is a modified version of the watermark elasticity policy from Monterey v3.
-    
-    private static final Logger LOG = LoggerFactory.getLogger(BalancingStrategy.class);
-    
-    private static final int MAX_MIGRATIONS_PER_BALANCING_NODE = 20; // arbitrary (Splodge)
-    private static final boolean BALANCE_COLD_PULLS_IN_SAME_RUN_AS_HOT_PUSHES = false;
-    
-    private final String name;
-    private final BalanceablePoolModel<NodeType, ItemType> model;
-    private final PolicyUtilForPool<NodeType, ItemType> helper;
-//    private boolean loggedColdestTooHigh = false;
-//    private boolean loggedHottestTooLow = false;
-    
-    
-    public BalancingStrategy(String name, BalanceablePoolModel<NodeType, ItemType> model) {
-        this.name = name;
-        this.model = model;
-        this.helper = new PolicyUtilForPool<NodeType, ItemType>(model);
-    }
-    
-    public String getName() {
-        return name;
-    }
-    
-    public void rebalance() {
-        checkAndApplyOn(model.getPoolContents());
-    }
-    
-    public int getMaxMigrationsPerBalancingNode() {
-        return MAX_MIGRATIONS_PER_BALANCING_NODE;
-    }
-    
-    public BalanceablePoolModel<NodeType, ItemType> getDataProvider() {
-        return model;
-    }
-    
-    // This was the entry point for the legacy policy.
-    private void checkAndApplyOn(final Collection<NodeType> dirtyNodesSupplied) {
-        Collection<NodeType> dirtyNodes = dirtyNodesSupplied;
-        
-//        if (startTime + FORCE_ALL_NODES_IF_DELAYED_FOR_MILLIS < System.currentTimeMillis()) {
-//            Set<NodeType> allNodes = new LinkedHashSet<NodeType>();
-//            allNodes.addAll(dirtyNodes);
-//            dirtyNodes = allNodes;
-//            allNodes.addAll(getDataProvider().getPoolContents());
-//            if (LOG.isDebugEnabled())
-//                LOG.debug("policy "+getDataProvider().getAbbr()+" running after delay ("+
-//                        TimeUtils.makeTimeString(System.currentTimeMillis() - startTime)+", analysing all nodes: "+
-//                        dirtyNodes);
-//        }
-        
-//        nodeFinder.optionalCachedNodesWithBacklogDetected.clear();
-//        boolean gonnaGrow = growPool(dirtyNodes);
-//        getDataProvider().waitForAllTransitionsComplete();
-        boolean gonnaGrow = false;
-        
-        Set<NodeType> nonFrozenDirtyNodes = new LinkedHashSet<NodeType>(dirtyNodes);
-//        boolean gonnaShrink = false;
-//        if (!gonnaGrow && !DONT_SHRINK_UNLESS_BALANCED) {
-//            gonnaShrink = shrinkPool(nonFrozenDirtyNodes);
-//            getDataProvider().waitForAllTransitionsComplete();
-//        }
-        
-        if (getDataProvider().getPoolSize() >= 2) {
-            boolean didBalancing = false;
-            for (NodeType a : nonFrozenDirtyNodes) {
-                didBalancing |= balanceItemsOnNodesInQuestion(a, gonnaGrow);
-//                getMutator().waitForAllTransitionsComplete();
-            }
-            if (didBalancing) {
-                return;
-            }
-        }
-        
-//        if (!gonnaGrow && DONT_SHRINK_UNLESS_BALANCED) {
-//            gonnaShrink = shrinkPool(nonFrozenDirtyNodes);
-//            getDataProvider().waitForAllTransitionsComplete();
-//        }
-        
-//        if (gonnaGrow || gonnaShrink)
-//        //don't log 'doing nothing' message
-//        return;
-        
-//        if (LOG.isDebugEnabled()) {
-//            double poolTotal = getDataProvider().getPoolPredictedWorkrateTotal();
-//            int poolSize = getDataProvider().getPoolPredictedSize();
-//            LOG.debug(MessageFormat.format("policy "+getDataProvider().getAbbr()+" did nothing; pool workrate is {0,number,#.##} x {1} nodes",
-//                    1.0*poolTotal/poolSize, poolSize));
-//        }
-    }
-    
-    protected boolean balanceItemsOnNodesInQuestion(NodeType questionedNode, boolean gonnaGrow) {
-        double questionedNodeTotalWorkrate = getDataProvider().getTotalWorkrate(questionedNode);
-        
-        boolean balanced = balanceItemsOnHotNode(questionedNode, questionedNodeTotalWorkrate, gonnaGrow);
-//        getMutator().waitForAllTransitionsComplete();
-        
-        if (!balanced || BALANCE_COLD_PULLS_IN_SAME_RUN_AS_HOT_PUSHES) {
-            balanced |= balanceItemsOnColdNode(questionedNode, questionedNodeTotalWorkrate, gonnaGrow);
-//            getMutator().waitForAllTransitionsComplete();
-        }
-        if (balanced)
-            return true;
-        
-        if (LOG.isDebugEnabled()) {
-            LOG.debug( MessageFormat.format(
-                    "policy "+getDataProvider().getName()+" not balancing "+questionedNode+"; " +
-                    "its workrate {0,number,#.##} is acceptable (or cannot be balanced)", questionedNodeTotalWorkrate) );
-        }
-        return false;
-    }
-    
-    protected boolean balanceItemsOnHotNode(NodeType node, double nodeWorkrate, boolean gonnaGrow) {
-        double originalNodeWorkrate = nodeWorkrate;
-        int migrationCount = 0;
-        int iterationCount = 0;
-        
-        Set<ItemType> itemsMoved = new LinkedHashSet<ItemType>();
-        Set<NodeType> nodesChecked = new LinkedHashSet<NodeType>();
-        
-//        if (nodeFinder.config.COUNT_BACKLOG_AS_EXTRA_WORKRATE) {
-//            int backlog = nodeFinder.getBacklogQueueLength(questionedNode);
-//            if (backlog>0) {
-//                Level l = backlog>1000 ? Level.INFO : backlog>10 ? Level.FINE : Level.FINER;
-//                if (LOG.isLoggable(l)) {
-//                    LOG.log( l, MessageFormat.format(
-//                            "policy "+getDataProvider().getAbbr()+" detected queue at node "+questionedNode+", " +
-//                            "inflating workrate {0,number,#.##} + "+backlog, questionedNodeTotalWorkrate) );
-//                }
-//                questionedNodeTotalWorkrate += backlog;
-//            }
-//        }
-        
-        Double highThreshold = model.getHighThreshold(node);
-        if (highThreshold == -1) {
-            // node presumably has been removed; TODO log
-            return false;
-        }
-        
-        while (nodeWorkrate > highThreshold && migrationCount < getMaxMigrationsPerBalancingNode()) {
-            iterationCount++;
-            
-            if (LOG.isDebugEnabled()) {
-                LOG.debug(MessageFormat.format(
-                        "policy "+getDataProvider().getName()+" considering balancing hot node "+node+" " +
-                        "(workrate {0,number,#.##}); iteration "+iterationCount, nodeWorkrate) );
-            }
-            
-            // move from hot node, to coldest
-            
-            NodeType coldNode = helper.findColdestContainer(nodesChecked);
-            
-            if (coldNode == null) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug( MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" not balancing hot node "+node+" " +
-                            "(workrate {0,number,#.##}); no coldest node available", nodeWorkrate) );
-                }
-                break;
-            }
-            
-            if (coldNode.equals(node)) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug( MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" not balancing hot node "+node+" " +
-                            "(workrate {0,number,#.##}); it is also the coldest modifiable node", nodeWorkrate) );
-                }
-                break;
-            }
-            
-            double coldNodeWorkrate = getDataProvider().getTotalWorkrate(coldNode);
-            boolean emergencyLoadBalancing = coldNodeWorkrate < nodeWorkrate*2/3;
-            double coldNodeHighThreshold = model.getHighThreshold(coldNode);
-            if (coldNodeWorkrate >= coldNodeHighThreshold && !emergencyLoadBalancing) {
-                //don't balance if all nodes are approx equally hot (and very hot)
-                
-                //for now, stop warning if it is a recurring theme!
-//                Level level = loggedColdestTooHigh ? Level.FINER : Level.INFO;
-//                LOG.log(level, MessageFormat.format(
-//                        "policy "+getDataProvider().getAbbr()+" not balancing hot node "+questionedNode+" " +
-//                        "(workrate {0,number,#.##}); coldest node "+coldNode+" has workrate {1,number,#.##} also too high"+
-//                        (loggedColdestTooHigh ? "" : " (future cases will be logged at finer)"),
-//                        questionedNodeTotalWorkrate, coldNodeWorkrate) );
-//                loggedColdestTooHigh = true;
-                break;
-            }
-            double poolLowWatermark = Double.MAX_VALUE; // TODO
-            if (gonnaGrow && (coldNodeWorkrate >= poolLowWatermark && !emergencyLoadBalancing)) {
-                //if we're growing the pool, refuse to balance unless the cold node is indeed very cold, or hot node very hot
-                
-                //for now, stop warning if it is a recurring theme!
-//                Level level = loggedColdestTooHigh ? Level.FINER : Level.INFO;
-//                LOG.log(level, MessageFormat.format(
-//                        "policy "+getDataProvider().getAbbr()+" not balancing hot node "+questionedNode+" " +
-//                        "(workrate {0,number,#.##}); coldest node "+coldNode+" has workrate {1,number,#.##} also too high to accept while pool is growing" +
-//                        (loggedColdestTooHigh ? "" : " (future cases will be logged at finer)"),
-//                        questionedNodeTotalWorkrate, coldNodeWorkrate) );
-//                loggedColdestTooHigh = true;
-                break;
-            }
-            
-            String questionedNodeName = getDataProvider().getName(node);
-            String coldNodeName = getDataProvider().getName(coldNode);
-            Location coldNodeLocation = getDataProvider().getLocation(coldNode);
-            
-            if (LOG.isDebugEnabled()) {
-                LOG.debug( MessageFormat.format(
-                        "policy "+getDataProvider().getName()+" balancing hot node "+questionedNodeName+" " +
-                        "("+node+", workrate {0,number,#.##}), " +
-                        "considering target "+coldNodeName+" ("+coldNode+", workrate {1,number,#.##})",
-                        nodeWorkrate, coldNodeWorkrate) );
-            }
-            
-            double idealSizeToMove = (nodeWorkrate - coldNodeWorkrate) / 2;
-            //if the 'ideal' amount to move would cause cold to be too hot, then reduce ideal amount
-            
-            if (idealSizeToMove + coldNodeWorkrate > coldNodeHighThreshold)
-                idealSizeToMove = coldNodeHighThreshold - coldNodeWorkrate;
-            
-            
-            double maxSizeToMoveIdeally = Math.min(
-                    nodeWorkrate/2 + 0.00001,
-                    //permit it to exceed node high if there is no alternative (this is 'max' not 'ideal'),
-                    //so long as it still gives a significant benefit
-                    //                      getConfiguration().nodeHighWaterMark - coldNodeWorkrate,
-                    (nodeWorkrate - coldNodeWorkrate)*0.9);
-            double maxSizeToMoveIfNoSmallButLarger = nodeWorkrate*3/4;
-            
-            Map<ItemType, Double> questionedNodeItems = getDataProvider().getItemWorkrates(node);
-            if (questionedNodeItems == null) {
-                if (LOG.isDebugEnabled())
-                    LOG.debug(MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" balancing hot node "+questionedNodeName+" " +
-                            "("+node+", workrate {0,number,#.##}), abandoned; " +
-                            "item report for " + questionedNodeName + " unavailable",
-                            nodeWorkrate));
-                break;
-            }
-            ItemType itemToMove = findBestItemToMove(questionedNodeItems, idealSizeToMove, maxSizeToMoveIdeally,
-                    maxSizeToMoveIfNoSmallButLarger, itemsMoved, coldNodeLocation);
-            
-            if (itemToMove == null) {
-                if (LOG.isDebugEnabled())
-                    LOG.debug(MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" balancing hot node "+questionedNodeName+" " +
-                            "("+node+", workrate {0,number,#.##}), ending; " +
-                            "no suitable segment found " +
-                            "(ideal transition item size {1,number,#.##}, max {2,number,#.##}, " +
-                            "moving to coldest node "+coldNodeName+" ("+coldNode+", workrate {3,number,#.##}); available items: {4}",
-                            nodeWorkrate, idealSizeToMove, maxSizeToMoveIdeally, coldNodeWorkrate, questionedNodeItems) );
-                break;
-            }
-            
-            itemsMoved.add(itemToMove);
-            double itemWorkrate = questionedNodeItems.get(itemToMove);
-            
-//            if (LOG.isLoggable(Level.FINE))
-//                LOG.fine( MessageFormat.format(
-//                        "policy "+getDataProvider().getAbbr()+" balancing hot node "+questionedNodeName+" " +
-//                        "(workrate {0,number,#.##}, too high), transitioning " + itemToMove +
-//                        " to "+coldNodeName+" (workrate {1,number,#.##}, now += {2,number,#.##})",
-//                        questionedNodeTotalWorkrate, coldNodeWorkrate, segmentRate) );
-            
-            nodeWorkrate -= itemWorkrate;
-            coldNodeWorkrate += itemWorkrate;
-            
-            moveItem(itemToMove, node, coldNode);
-            ++migrationCount;
-        }
-        
-        if (LOG.isDebugEnabled()) {
-            if (iterationCount == 0) {
-                if (LOG.isTraceEnabled())
-                    LOG.trace( MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" balancing if hot finished at node "+node+"; " +
-                            "workrate {0,number,#.##} not hot",
-                            originalNodeWorkrate) );
-            }
-            else if (itemsMoved.isEmpty()) {
-                if (LOG.isTraceEnabled())
-                    LOG.trace( MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" balancing finished at hot node "+node+" " +
-                            "(workrate {0,number,#.##}); no way to improve it",
-                            originalNodeWorkrate) );
-            } else {
-                LOG.debug( MessageFormat.format(
-                        "policy "+getDataProvider().getName()+" balancing finished at hot node "+node+"; " +
-                        "workrate from {0,number,#.##} to {1,number,#.##} (report now says {2,number,#.##}) " +
-                        "by moving off {3}",
-                        originalNodeWorkrate,
-                        nodeWorkrate,
-                        getDataProvider().getTotalWorkrate(node),
-                        itemsMoved
-                        ) );
-            }
-        }
-        return !itemsMoved.isEmpty();
-    }
-    
-    protected boolean balanceItemsOnColdNode(NodeType questionedNode, double questionedNodeTotalWorkrate, boolean gonnaGrow) {
-        // Abort if the node has pending adjustments.
-        Map<ItemType, Double> items = getDataProvider().getItemWorkrates(questionedNode);
-        if (items == null) {
-            if (LOG.isDebugEnabled()) {
-                LOG.debug( MessageFormat.format(
-                        "policy "+getDataProvider().getName()+" not balancing cold node "+questionedNode+" " +
-                        "(workrate {0,number,#.##}); workrate breakdown unavailable (probably reverting)",
-                        questionedNodeTotalWorkrate) );
-            }
-            return false;
-        }
-        for (ItemType item : items.keySet()) {
-            if (!model.isItemMoveable(item)) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug( MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" not balancing cold node "+questionedNode+" " +
-                            "(workrate {0,number,#.##}); at least one item ("+item+") is in flux",
-                            questionedNodeTotalWorkrate) );
-                }
-                return false;
-            }
-        }
-        
-        double originalQuestionedNodeTotalWorkrate = questionedNodeTotalWorkrate;
-        int numMigrations = 0;
-        
-        Set<ItemType> itemsMoved = new LinkedHashSet<ItemType>();
-        Set<NodeType> nodesChecked = new LinkedHashSet<NodeType>();
-        
-        int iters = 0;
-        Location questionedLocation = getDataProvider().getLocation(questionedNode);
-        
-        double lowThreshold = model.getLowThreshold(questionedNode);
-        while (questionedNodeTotalWorkrate < lowThreshold) {
-            iters++;
-            
-            if (LOG.isDebugEnabled()) {
-                LOG.debug( MessageFormat.format(
-                        "policy "+getDataProvider().getName()+" considering balancing cold node "+questionedNode+" " +
-                        "(workrate {0,number,#.##}); iteration "+iters, questionedNodeTotalWorkrate));
-            }
-            
-            // move from cold node, to hottest
-            
-            NodeType hotNode = helper.findHottestContainer(nodesChecked);
-            
-            if (hotNode == null) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug( MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" not balancing cold node "+questionedNode+" " +
-                            "(workrate {0,number,#.##}); no hottest node available", questionedNodeTotalWorkrate) );
-                }
-                
-                break;
-            }
-            if (hotNode.equals(questionedNode)) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug( MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" not balancing cold node "+questionedNode+" " +
-                            "(workrate {0,number,#.##}); it is also the hottest modfiable node", questionedNodeTotalWorkrate) );
-                }
-                break;
-            }
-            
-            
-            double hotNodeWorkrate = getDataProvider().getTotalWorkrate(hotNode);
-            double hotNodeLowThreshold = model.getLowThreshold(hotNode);
-            double hotNodeHighThreshold = model.getHighThreshold(hotNode);
-            boolean emergencyLoadBalancing = false;  //doesn't apply to cold
-            if (hotNodeWorkrate == -1 || hotNodeLowThreshold == -1 || hotNodeHighThreshold == -1) {
-                // hotNode presumably has been removed; TODO log
-                break;
-            }
-            if (hotNodeWorkrate <= hotNodeLowThreshold && !emergencyLoadBalancing) {
-                //don't balance if all nodes are too low
-                
-                //for now, stop warning if it is a recurring theme!
-//                Level level = loggedHottestTooLow ? Level.FINER : Level.INFO;
-//                LOG.log(level, MessageFormat.format(
-//                        "policy "+getDataProvider().getAbbr()+" not balancing cold node "+questionedNode+" " +
-//                        "(workrate {0,number,#.##}); hottest node "+hotNode+" has workrate {1,number,#.##} also too low" +
-//                        (loggedHottestTooLow ? "" : " (future cases will be logged at finer)"),
-//                        questionedNodeTotalWorkrate, hotNodeWorkrate) );
-//                loggedHottestTooLow = true;
-                break;
-            }
-            if (gonnaGrow && (hotNodeWorkrate <= hotNodeHighThreshold && !emergencyLoadBalancing)) {
-                //if we're growing the pool, refuse to balance unless the hot node is quite hot
-                
-                //again, stop warning if it is a recurring theme!
-//                Level level = loggedHottestTooLow ? Level.FINER : Level.INFO;
-//                LOG.log(level, MessageFormat.format(
-//                        "policy "+getDataProvider().getAbbr()+" not balancing cold node "+questionedNode+" " +
-//                        "(workrate {0,number,#.##}); hottest node "+hotNode+" has workrate {1,number,#.##} also too low to accept while pool is growing"+
-//                        (loggedHottestTooLow ? "" : " (future cases will be logged at finer)"),
-//                        questionedNodeTotalWorkrate, hotNodeWorkrate) );
-//                loggedHottestTooLow = true;
-                break;
-            }
-            
-            String questionedNodeName = getDataProvider().getName(questionedNode);
-            String hotNodeName = getDataProvider().getName(hotNode);
-            
-            if (LOG.isDebugEnabled()) {
-                LOG.debug( MessageFormat.format(
-                        "policy "+getDataProvider().getName()+" balancing cold node "+questionedNodeName+" " +
-                        "("+questionedNode+", workrate {0,number,#.##}), " +
-                        "considering source "+hotNodeName+" ("+hotNode+", workrate {1,number,#.##})",
-                        questionedNodeTotalWorkrate, hotNodeWorkrate) );
-            }
-            
-            double idealSizeToMove = (hotNodeWorkrate - questionedNodeTotalWorkrate) / 2;
-            //if the 'ideal' amount to move would cause cold to be too hot, then reduce ideal amount
-            double targetNodeHighThreshold = model.getHighThreshold(questionedNode);
-            if (idealSizeToMove + questionedNodeTotalWorkrate > targetNodeHighThreshold)
-                idealSizeToMove = targetNodeHighThreshold - questionedNodeTotalWorkrate;
-            double maxSizeToMoveIdeally = Math.min(
-                    hotNodeWorkrate/2,
-                    //allow to swap order, but not very much (0.9 was allowed when balancing high)
-                    (hotNodeWorkrate - questionedNodeTotalWorkrate)*0.6);
-            double maxSizeToMoveIfNoSmallButLarger = questionedNodeTotalWorkrate*3/4;
-            
-            Map<ItemType, Double> hotNodeItems = getDataProvider().getItemWorkrates(hotNode);
-            if (hotNodeItems == null) {
-                if (LOG.isDebugEnabled())
-                    LOG.debug(MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" balancing cold node "+questionedNodeName+" " +
-                            "("+questionedNode+", workrate {0,number,#.##}), " +
-                            "excluding hot node "+hotNodeName+" because its item report unavailable",
-                            questionedNodeTotalWorkrate));
-                nodesChecked.add(hotNode);
-                continue;
-            }
-            
-            ItemType itemToMove = findBestItemToMove(hotNodeItems, idealSizeToMove, maxSizeToMoveIdeally,
-                    maxSizeToMoveIfNoSmallButLarger, itemsMoved, questionedLocation);
-            if (itemToMove == null) {
-                if (LOG.isDebugEnabled())
-                    LOG.debug(MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" balancing cold node "+questionedNodeName+" " +
-                            "("+questionedNode+", workrate {0,number,#.##}), " +
-                            "excluding hot node "+hotNodeName+" because it has no appilcable items " +
-                            "(ideal transition item size {1,number,#.##}, max {2,number,#.##}, " +
-                            "moving from hot node "+hotNodeName+" ("+hotNode+", workrate {3,number,#.##}); available items: {4}",
-                            questionedNodeTotalWorkrate, idealSizeToMove, maxSizeToMoveIdeally, hotNodeWorkrate, hotNodeItems) );
-                
-                nodesChecked.add(hotNode);
-                continue;
-            }
-            
-            itemsMoved.add(itemToMove);
-            double segmentRate = hotNodeItems.get(itemToMove);
-            
-//            if (LOG.isLoggable(Level.FINE))
-//                LOG.fine( MessageFormat.format(
-//                        "policy "+getDataProvider().getAbbr()+" balancing cold node "+questionedNodeName+" " +
-//                        "(workrate {0,number,#.##}, too low), transitioning " + itemToMove +
-//                        " from "+hotNodeName+" (workrate {1,number,#.##}, now -= {2,number,#.##})",
-//                        questionedNodeTotalWorkrate, hotNodeWorkrate, segmentRate) );
-            
-            questionedNodeTotalWorkrate += segmentRate;
-            hotNodeWorkrate -= segmentRate;
-            
-            moveItem(itemToMove, hotNode, questionedNode);
-            
-            if (++numMigrations >= getMaxMigrationsPerBalancingNode()) {
-                break;
-            }
-        }
-        
-        if (LOG.isDebugEnabled()) {
-            if (iters == 0) {
-                if (LOG.isTraceEnabled())
-                    LOG.trace( MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" balancing if cold finished at node "+questionedNode+"; " +
-                            "workrate {0,number,#.##} not cold",
-                            originalQuestionedNodeTotalWorkrate) );
-            }
-            else if (itemsMoved.isEmpty()) {
-                if (LOG.isTraceEnabled())
-                    LOG.trace( MessageFormat.format(
-                            "policy "+getDataProvider().getName()+" balancing finished at cold node "+questionedNode+" " +
-                            "(workrate {0,number,#.##}); no way to improve it",
-                            originalQuestionedNodeTotalWorkrate) );
-            } else {
-                LOG.debug( MessageFormat.format(
-                        "policy "+getDataProvider().getName()+" balancing finished at cold node "+questionedNode+"; " +
-                        "workrate from {0,number,#.##} to {1,number,#.##} (report now says {2,number,#.##}) " +
-                        "by moving in {3}",
-                        originalQuestionedNodeTotalWorkrate,
-                        questionedNodeTotalWorkrate,
-                        getDataProvider().getTotalWorkrate(questionedNode),
-                        itemsMoved) );
-            }
-        }
-        return !itemsMoved.isEmpty();
-    }
-    
-    protected void moveItem(ItemType item, NodeType oldNode, NodeType newNode) {
-        item.move(newNode);
-        model.onItemMoved(item, newNode);
-    }
-    
-    /**
-     * "Best" is defined as nearest to the targetCost, without exceeding maxCost, unless maxCostIfNothingSmallerButLarger > 0
-     * which does just that (useful if the ideal and target are estimates and aren't quite right, typically it will take
-     * something larger than maxRate but less than half the total rate, which is only possible when the estimates don't agree)
-     */
-    protected ItemType findBestItemToMove(Map<ItemType, Double> costsPerItem, double targetCost, double maxCost,
-            double maxCostIfNothingSmallerButLarger, Set<ItemType> excludedItems, Location locationIfKnown) {
-        
-        ItemType closestMatch = null;
-        ItemType smallestMoveable = null, largest = null;
-        double minDiff = Double.MAX_VALUE, smallestC = Double.MAX_VALUE, largestC = Double.MIN_VALUE;
-        boolean exclusions = false;
-        
-        for (Entry<ItemType, Double> entry : costsPerItem.entrySet()) {
-            ItemType item = entry.getKey();
-            Double cost = entry.getValue();
-            
-            if (cost == null) {
-                if (LOG.isDebugEnabled()) LOG.debug(MessageFormat.format("Item ''{0}'' has null workrate: skipping", item));
-                continue;
-            }
-            
-            if (!model.isItemMoveable(item)) {
-                if (LOG.isDebugEnabled()) LOG.debug(MessageFormat.format("Item ''{0}'' cannot be moved: skipping", item));
-                continue;
-            }
-            if (cost < 0) {
-                if (LOG.isDebugEnabled()) LOG.debug(MessageFormat.format("Item ''{0}'' subject to recent adjustment: skipping", item));
-                continue;
-            }
-            if (excludedItems.contains(item)) {
-                exclusions = true;
-                continue;
-            }
-            if (cost < 0) { // FIXME: already tested above
-                exclusions = true;
-                continue;
-            }
-            if (cost <= 0) { // FIXME: overlaps previous condition
-                continue;
-            }
-            if (largest == null || cost > largestC) {
-                largest = item;
-                largestC = cost;
-            }
-            if (!model.isItemMoveable(item)) { // FIXME: already tested above
-                continue;
-            }
-            if (locationIfKnown != null && !model.isItemAllowedIn(item, locationIfKnown)) {
-                continue;
-            }
-            if (smallestMoveable == null || cost < smallestC) {
-                smallestMoveable = item;
-                smallestC = cost;
-            }
-            if (cost > maxCost) {
-                continue;
-            }
-            double diff = Math.abs(targetCost - cost);
-            if (closestMatch == null || diff < minDiff) {
-                closestMatch = item;
-                minDiff = diff;
-            }
-        }
-        
-        if (closestMatch != null)
-            return closestMatch;
-        
-        if (smallestC < maxCostIfNothingSmallerButLarger && smallestC < largestC && !exclusions)
-            return smallestMoveable;
-        
-        return null;
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/DefaultBalanceablePoolModel.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/DefaultBalanceablePoolModel.java b/policy/src/main/java/brooklyn/policy/loadbalancing/DefaultBalanceablePoolModel.java
deleted file mode 100644
index 7e273ba..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/DefaultBalanceablePoolModel.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import java.io.ByteArrayOutputStream;
-import java.io.PrintStream;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.brooklyn.api.location.Location;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Multimaps;
-import com.google.common.collect.SetMultimap;
-
-/**
- * Standard implementation of {@link BalanceablePoolModel}, providing essential arithmetic for item and container
- * workrates and thresholds. See subclasses for specific requirements for migrating items.
- */
-public class DefaultBalanceablePoolModel<ContainerType, ItemType> implements BalanceablePoolModel<ContainerType, ItemType> {
-    
-    private static final Logger LOG = LoggerFactory.getLogger(DefaultBalanceablePoolModel.class);
-    
-    /*
-     * Performance comments.
-     *  - Used hprof with LoadBalancingPolicySoakTest.testLoadBalancingManyManyItemsTest (1000 items)
-     *  - Prior to adding containerToItems, it created a new set by iterating over all items.
-     *    This was the biggest percentage of any brooklyn code.
-     *    Hence it's worth duplicating the values, keyed by item and keyed by container.
-     *  - Unfortunately changing threading model (so have a "rebalancer" thread, and a thread that 
-     *    processes events to update the model), get ConcurrentModificationException if don't take
-     *    copy of containerToItems.get(node)...
-     */
-    
-    // Concurrent maps cannot have null value; use this to represent when no container is supplied for an item 
-    private static final String NULL_CONTAINER = "null-container";
-    
-    private final String name;
-    private final Set<ContainerType> containers = Collections.newSetFromMap(new ConcurrentHashMap<ContainerType,Boolean>());
-    private final Map<ContainerType, Double> containerToLowThreshold = new ConcurrentHashMap<ContainerType, Double>();
-    private final Map<ContainerType, Double> containerToHighThreshold = new ConcurrentHashMap<ContainerType, Double>();
-    private final Map<ItemType, ContainerType> itemToContainer = new ConcurrentHashMap<ItemType, ContainerType>();
-    private final SetMultimap<ContainerType, ItemType> containerToItems =  Multimaps.synchronizedSetMultimap(HashMultimap.<ContainerType, ItemType>create());
-    private final Map<ItemType, Double> itemToWorkrate = new ConcurrentHashMap<ItemType, Double>();
-    private final Set<ItemType> immovableItems = Collections.newSetFromMap(new ConcurrentHashMap<ItemType, Boolean>());
-    
-    private volatile double poolLowThreshold = 0;
-    private volatile double poolHighThreshold = 0;
-    private volatile double currentPoolWorkrate = 0;
-    
-    public DefaultBalanceablePoolModel(String name) {
-        this.name = name;
-    }
-    
-    public ContainerType getParentContainer(ItemType item) {
-        ContainerType result = itemToContainer.get(item);
-        return (result != NULL_CONTAINER) ? result : null;
-    }
-    
-    public Set<ItemType> getItemsForContainer(ContainerType node) {
-        Set<ItemType> result = containerToItems.get(node);
-        synchronized (containerToItems) {
-            return (result != null) ? ImmutableSet.copyOf(result) : Collections.<ItemType>emptySet();
-        }
-    }
-    
-    public Double getItemWorkrate(ItemType item) {
-        return itemToWorkrate.get(item);
-    }
-    
-    @Override public double getPoolLowThreshold() { return poolLowThreshold; }
-    @Override public double getPoolHighThreshold() { return poolHighThreshold; }
-    @Override public double getCurrentPoolWorkrate() { return currentPoolWorkrate; }
-    @Override public boolean isHot() { return !containers.isEmpty() && currentPoolWorkrate > poolHighThreshold; }
-    @Override public boolean isCold() { return !containers.isEmpty() && currentPoolWorkrate < poolLowThreshold; }
-    
-    
-    // Provider methods.
-    
-    @Override public String getName() { return name; }
-    @Override public int getPoolSize() { return containers.size(); }
-    @Override public Set<ContainerType> getPoolContents() { return containers; }
-    @Override public String getName(ContainerType container) { return container.toString(); } // TODO: delete?
-    @Override public Location getLocation(ContainerType container) { return null; } // TODO?
-    
-    @Override public double getLowThreshold(ContainerType container) {
-        Double result = containerToLowThreshold.get(container);
-        return (result != null) ? result : -1;
-    }
-    
-    @Override public double getHighThreshold(ContainerType container) {
-        Double result = containerToHighThreshold.get(container);
-        return (result != null) ? result : -1;
-    }
-    
-    @Override public double getTotalWorkrate(ContainerType container) {
-        double totalWorkrate = 0;
-        for (ItemType item : getItemsForContainer(container)) {
-            Double workrate = itemToWorkrate.get(item);
-            if (workrate != null)
-                totalWorkrate += Math.abs(workrate);
-        }
-        return totalWorkrate;
-    }
-    
-    @Override public Map<ContainerType, Double> getContainerWorkrates() {
-        Map<ContainerType, Double> result = new LinkedHashMap<ContainerType, Double>();
-        for (ContainerType node : containers)
-            result.put(node, getTotalWorkrate(node));
-        return result;
-    }
-    
-    @Override public Map<ItemType, Double> getItemWorkrates(ContainerType node) {
-        Map<ItemType, Double> result = new LinkedHashMap<ItemType, Double>();
-        for (ItemType item : getItemsForContainer(node))
-            result.put(item, itemToWorkrate.get(item));
-        return result;
-    }
-    
-    @Override public boolean isItemMoveable(ItemType item) {
-        // If don't know about item, then assume not movable; otherwise has this item been explicitly flagged as immovable?
-        return itemToContainer.containsKey(item) && !immovableItems.contains(item);
-    }
-    
-    @Override public boolean isItemAllowedIn(ItemType item, Location location) {
-        return true; // TODO?
-    }
-    
-    
-    // Mutators.
-    
-    @Override
-    public void onItemMoved(ItemType item, ContainerType newNode) {
-        if (!itemToContainer.containsKey(item)) {
-            // Item may have been deleted; order of events received from different sources 
-            // (i.e. item itself and for itemGroup membership) is non-deterministic.
-            LOG.info("Balanceable pool model ignoring onItemMoved for unknown item {} to container {}; " +
-                    "if onItemAdded subsequently received will get new container then", item, newNode);
-            return;
-        }
-        ContainerType newNodeNonNull = toNonNullContainer(newNode);
-        ContainerType oldNode = itemToContainer.put(item, newNodeNonNull);
-        if (oldNode != null && oldNode != NULL_CONTAINER) containerToItems.remove(oldNode, item);
-        if (newNode != null) containerToItems.put(newNode, item);
-    }
-    
-    @Override
-    public void onContainerAdded(ContainerType newContainer, double lowThreshold, double highThreshold) {
-        boolean added = containers.add(newContainer);
-        if (!added) {
-            // See LoadBalancingPolicy.onContainerAdded for possible explanation of why can get duplicate calls
-            LOG.debug("Duplicate container-added event for {}; ignoring", newContainer);
-            return;
-        }
-        containerToLowThreshold.put(newContainer, lowThreshold);
-        containerToHighThreshold.put(newContainer, highThreshold);
-        poolLowThreshold += lowThreshold;
-        poolHighThreshold += highThreshold;
-    }
-    
-    @Override
-    public void onContainerRemoved(ContainerType oldContainer) {
-        containers.remove(oldContainer);
-        Double containerLowThreshold = containerToLowThreshold.remove(oldContainer);
-        Double containerHighThresold = containerToHighThreshold.remove(oldContainer);
-        poolLowThreshold -= (containerLowThreshold != null ? containerLowThreshold : 0);
-        poolHighThreshold -= (containerHighThresold != null ? containerHighThresold : 0);
-        
-        // TODO: assert no orphaned items
-    }
-    
-    @Override
-    public void onItemAdded(ItemType item, ContainerType parentContainer) {
-        onItemAdded(item, parentContainer, false);
-    }
-    
-    @Override
-    public void onItemAdded(ItemType item, ContainerType parentContainer, boolean immovable) {
-        // Duplicate calls to onItemAdded do no harm, as long as most recent is most accurate!
-        // Important that it stays that way for now - See LoadBalancingPolicy.onContainerAdded for explanation.
-
-        if (immovable)
-            immovableItems.add(item);
-        
-        ContainerType parentContainerNonNull = toNonNullContainer(parentContainer);
-        ContainerType oldNode = itemToContainer.put(item, parentContainerNonNull);
-        if (oldNode != null && oldNode != NULL_CONTAINER) containerToItems.remove(oldNode, item);
-        if (parentContainer != null) containerToItems.put(parentContainer, item);
-    }
-    
-    @Override
-    public void onItemRemoved(ItemType item) {
-        ContainerType oldNode = itemToContainer.remove(item);
-        if (oldNode != null && oldNode != NULL_CONTAINER) containerToItems.remove(oldNode, item);
-        Double workrate = itemToWorkrate.remove(item);
-        if (workrate != null)
-            currentPoolWorkrate -= workrate;
-        immovableItems.remove(item);
-    }
-    
-    @Override
-    public void onItemWorkrateUpdated(ItemType item, double newValue) {
-        if (hasItem(item)) {
-            Double oldValue = itemToWorkrate.put(item, newValue);
-            double delta = ( newValue - (oldValue != null ? oldValue : 0) );
-            currentPoolWorkrate += delta;
-        } else {
-            // Can happen when item removed - get notification of removal and workrate from group and item
-            // respectively, so can overtake each other
-            if (LOG.isDebugEnabled()) LOG.debug("Ignoring setting of workrate for unknown item {}, to {}", item, newValue);
-        }
-    }
-    
-    private boolean hasItem(ItemType item) {
-        return itemToContainer.containsKey(item);
-    }
-    
-    
-    // Additional methods for tests.
-
-    /**
-     * Warning: this can be an expensive (time and memory) operation if there are a lot of items/containers. 
-     */
-    @VisibleForTesting
-    public String itemDistributionToString() {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        dumpItemDistribution(new PrintStream(baos));
-        return new String(baos.toByteArray());
-    }
-
-    @VisibleForTesting
-    public void dumpItemDistribution() {
-        dumpItemDistribution(System.out);
-    }
-    
-    @VisibleForTesting
-    public void dumpItemDistribution(PrintStream out) {
-        for (ContainerType container : getPoolContents()) {
-            out.println("Container '"+container+"': ");
-            for (ItemType item : getItemsForContainer(container)) {
-                Double workrate = getItemWorkrate(item);
-                out.println("\t"+"Item '"+item+"' ("+workrate+")");
-            }
-        }
-        out.flush();
-    }
-    
-    @SuppressWarnings("unchecked")
-    private ContainerType nullContainer() {
-        return (ContainerType) NULL_CONTAINER; // relies on erasure
-    }
-    
-    private ContainerType toNonNullContainer(ContainerType container) {
-        return (container != null) ? container : nullContainer();
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/ItemsInContainersGroup.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/ItemsInContainersGroup.java b/policy/src/main/java/brooklyn/policy/loadbalancing/ItemsInContainersGroup.java
deleted file mode 100644
index efd41d8..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/ItemsInContainersGroup.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.DynamicGroup;
-import brooklyn.event.basic.BasicConfigKey;
-
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-
-/**
- * A group of items that are contained within a given (dynamically changing) set of containers.
- * 
- * The {@link setContainers(Group)} sets the group of containers. The membership of that group
- * is dynamically tracked.
- * 
- * When containers are added/removed, or when an items is added/removed, or when an {@link Moveable} item 
- * is moved then the membership of this group of items is automatically updated accordingly.
- * 
- * For example: in Monterey, this could be used to track the actors that are within a given cluster of venues.
- */
-@ImplementedBy(ItemsInContainersGroupImpl.class)
-public interface ItemsInContainersGroup extends DynamicGroup {
-
-    @SetFromFlag("itemFilter")
-    public static final ConfigKey<Predicate<? super Entity>> ITEM_FILTER = new BasicConfigKey(
-            Predicate.class, "itemsInContainerGroup.itemFilter", "Filter for which items within the containers will automatically be in group", Predicates.alwaysTrue());
-
-    public void setContainers(Group containerGroup);
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/ItemsInContainersGroupImpl.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/ItemsInContainersGroupImpl.java b/policy/src/main/java/brooklyn/policy/loadbalancing/ItemsInContainersGroupImpl.java
deleted file mode 100644
index daf5dfe..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/ItemsInContainersGroupImpl.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.entity.basic.AbstractGroup;
-import brooklyn.entity.basic.DynamicGroupImpl;
-
-import com.google.common.base.Predicate;
-
-/**
- * A group of items that are contained within a given (dynamically changing) set of containers.
- * 
- * The {@link setContainers(Group)} sets the group of containers. The membership of that group
- * is dynamically tracked.
- * 
- * When containers are added/removed, or when an items is added/removed, or when an {@link Moveable} item 
- * is moved then the membership of this group of items is automatically updated accordingly.
- * 
- * For example: in Monterey, this could be used to track the actors that are within a given cluster of venues.
- */
-public class ItemsInContainersGroupImpl extends DynamicGroupImpl implements ItemsInContainersGroup {
-
-    // TODO Inefficient: will not scale to many 1000s of items
-
-    private static final Logger LOG = LoggerFactory.getLogger(ItemsInContainersGroup.class);
-    
-    private Group containerGroup;
-    
-    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
-        @Override
-        public void onEvent(SensorEvent<Object> event) {
-            Entity source = event.getSource();
-            Object value = event.getValue();
-            Sensor sensor = event.getSensor();
-            
-            if (sensor.equals(AbstractGroup.MEMBER_ADDED)) {
-                onContainerAdded((Entity) value);
-            } else if (sensor.equals(AbstractGroup.MEMBER_REMOVED)) {
-                onContainerRemoved((Entity) value);
-            } else if (sensor.equals(Movable.CONTAINER)) {
-                onItemMoved((Movable)source, (BalanceableContainer<?>) value);
-            } else {
-                throw new IllegalStateException("Unhandled event type "+sensor+": "+event);
-            }
-        }
-    };
-
-    public ItemsInContainersGroupImpl() {
-    }
-    
-    @Override
-    public void init() {
-        super.init();
-        setEntityFilter(new Predicate<Entity>() {
-            @Override public boolean apply(Entity e) {
-                return acceptsEntity(e);
-            }});
-    }
-    
-    protected Predicate<? super Entity> getItemFilter() {
-        return getConfig(ITEM_FILTER);
-    }
-    
-    @Override
-    protected boolean acceptsEntity(Entity e) {
-        if (e instanceof Movable) {
-            return acceptsItem((Movable)e, ((Movable)e).getAttribute(Movable.CONTAINER));
-        }
-        return false;
-    }
-
-    boolean acceptsItem(Movable e, BalanceableContainer c) {
-        return (containerGroup != null && c != null) ? getItemFilter().apply(e) && containerGroup.hasMember(c) : false;
-    }
-
-    @Override
-    public void setContainers(Group containerGroup) {
-        this.containerGroup = containerGroup;
-        subscribe(containerGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
-        subscribe(containerGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
-        subscribe(null, Movable.CONTAINER, eventHandler);
-        
-        if (LOG.isTraceEnabled()) LOG.trace("{} scanning entities on container group set", this);
-        rescanEntities();
-    }
-    
-    private void onContainerAdded(Entity newContainer) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} rescanning entities on container {} added", this, newContainer);
-        rescanEntities();
-    }
-    
-    private void onContainerRemoved(Entity oldContainer) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} rescanning entities on container {} removed", this, oldContainer);
-        rescanEntities();
-    }
-    
-    protected void onEntityAdded(Entity item) {
-        if (acceptsEntity(item)) {
-            if (LOG.isDebugEnabled()) LOG.debug("{} adding new item {}", this, item);
-            addMember(item);
-        }
-    }
-    
-    protected void onEntityRemoved(Entity item) {
-        if (removeMember(item)) {
-            if (LOG.isDebugEnabled()) LOG.debug("{} removing deleted item {}", this, item);
-        }
-    }
-    
-    private void onItemMoved(Movable item, BalanceableContainer container) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} processing moved item {}, to container {}", new Object[] {this, item, container});
-        if (hasMember(item)) {
-            if (!acceptsItem(item, container)) {
-                if (LOG.isDebugEnabled()) LOG.debug("{} removing moved item {} from group, as new container {} is not a member", new Object[] {this, item, container});
-                removeMember(item);
-            }
-        } else {
-            if (acceptsItem(item, container)) {
-                if (LOG.isDebugEnabled()) LOG.debug("{} adding moved item {} to group, as new container {} is a member", new Object[] {this, item, container});
-                addMember(item);
-            }
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/LoadBalancingPolicy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/LoadBalancingPolicy.java b/policy/src/main/java/brooklyn/policy/loadbalancing/LoadBalancingPolicy.java
deleted file mode 100644
index 57b736b..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/LoadBalancingPolicy.java
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import static brooklyn.util.JavaGroovyEquivalents.elvis;
-import static brooklyn.util.JavaGroovyEquivalents.groovyTruth;
-
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.event.AttributeSensor;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.EntityInternal;
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
-import brooklyn.policy.loadbalancing.BalanceableWorkerPool.ContainerItemPair;
-import brooklyn.util.collections.MutableMap;
-
-import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-
-
-/**
- * <p>Policy that is attached to a pool of "containers", each of which can host one or more migratable "items".
- * The policy monitors the workrates of the items and effects migrations in an attempt to ensure that the containers
- * are all sufficiently utilized without any of them being overloaded.
- * 
- * <p>The particular sensor that defines the items' workrates is specified when the policy is constructed. High- and
- * low-thresholds are defined as <strong>configuration keys</strong> on each of the container entities in the pool:
- * for an item sensor named {@code foo.bar.sensorName}, the corresponding container config keys would be named
- * {@code foo.bar.sensorName.threshold.low} and {@code foo.bar.sensorName.threshold.high}.
- * 
- * <p>In addition to balancing items among the available containers, this policy causes the pool Entity to emit
- * {@code POOL_COLD} and {@code POOL_HOT} events when it is determined that there is a surplus or shortfall
- * of container resource in the pool respectively. These events may be consumed by a separate policy that is capable
- * of resizing the container pool.
- */
-    // removed from catalog because it cannot currently be configured via catalog mechanisms
-    // PolicySpec.create fails due to no no-arg constructor
-    // TODO make metric and model things which can be initialized from config then reinstate in catalog
-//@Catalog(name="Load Balancer", description="Policy that is attached to a pool of \"containers\", each of which "
-//        + "can host one or more migratable \"items\". The policy monitors the workrates of the items and effects "
-//        + "migrations in an attempt to ensure that the containers are all sufficiently utilized without any of "
-//        + "them being overloaded.")
-public class LoadBalancingPolicy<NodeType extends Entity, ItemType extends Movable> extends AbstractPolicy {
-    
-    private static final Logger LOG = LoggerFactory.getLogger(LoadBalancingPolicy.class);
-    
-    @SetFromFlag(defaultVal="100")
-    private long minPeriodBetweenExecs;
-    
-    private final AttributeSensor<? extends Number> metric;
-    private final String lowThresholdConfigKeyName;
-    private final String highThresholdConfigKeyName;
-    private final BalanceablePoolModel<NodeType, ItemType> model;
-    private final BalancingStrategy<NodeType, ItemType> strategy;
-    private BalanceableWorkerPool poolEntity;
-    
-    private volatile ScheduledExecutorService executor;
-    private final AtomicBoolean executorQueued = new AtomicBoolean(false);
-    private volatile long executorTime = 0;
-
-    private int lastEmittedDesiredPoolSize = 0;
-    private static enum TemperatureStates { COLD, HOT }
-    private TemperatureStates lastEmittedPoolTemperature = null; // "cold" or "hot"
-    
-    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
-        @SuppressWarnings({ "rawtypes", "unchecked" })
-        public void onEvent(SensorEvent<Object> event) {
-            if (LOG.isTraceEnabled()) LOG.trace("{} received event {}", LoadBalancingPolicy.this, event);
-            Entity source = event.getSource();
-            Object value = event.getValue();
-            Sensor sensor = event.getSensor();
-            
-            if (sensor.equals(metric)) {
-                onItemMetricUpdate((ItemType)source, ((Number) value).doubleValue(), true);
-            } else if (sensor.equals(BalanceableWorkerPool.CONTAINER_ADDED)) {
-                onContainerAdded((NodeType) value, true);
-            } else if (sensor.equals(BalanceableWorkerPool.CONTAINER_REMOVED)) {
-                onContainerRemoved((NodeType) value, true);
-            } else if (sensor.equals(BalanceableWorkerPool.ITEM_ADDED)) {
-                ContainerItemPair pair = (ContainerItemPair) value;
-                onItemAdded((ItemType)pair.item, (NodeType)pair.container, true);
-            } else if (sensor.equals(BalanceableWorkerPool.ITEM_REMOVED)) {
-                ContainerItemPair pair = (ContainerItemPair) value;
-                onItemRemoved((ItemType)pair.item, (NodeType)pair.container, true);
-            } else if (sensor.equals(BalanceableWorkerPool.ITEM_MOVED)) {
-                ContainerItemPair pair = (ContainerItemPair) value;
-                onItemMoved((ItemType)pair.item, (NodeType)pair.container, true);
-            }
-        }
-    };
-
-    public LoadBalancingPolicy() {
-        this(null, null);
-    }
-    
-    public LoadBalancingPolicy(AttributeSensor<? extends Number> metric,
-            BalanceablePoolModel<NodeType, ItemType> model) {
-        this(MutableMap.of(), metric, model);
-    }
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    public LoadBalancingPolicy(Map props, AttributeSensor<? extends Number> metric,
-            BalanceablePoolModel<NodeType, ItemType> model) {
-        
-        super(props);
-        this.metric = metric;
-        this.lowThresholdConfigKeyName = metric.getName()+".threshold.low";
-        this.highThresholdConfigKeyName = metric.getName()+".threshold.high";
-        this.model = model;
-        this.strategy = new BalancingStrategy(getDisplayName(), model); // TODO: extract interface, inject impl
-        
-        // TODO Should re-use the execution manager's thread pool, somehow
-        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
-    }
-    
-    @SuppressWarnings("unchecked")
-    @Override
-    public void setEntity(EntityLocal entity) {
-        Preconditions.checkArgument(entity instanceof BalanceableWorkerPool, "Provided entity must be a BalanceableWorkerPool");
-        super.setEntity(entity);
-        this.poolEntity = (BalanceableWorkerPool) entity;
-        
-        // Detect when containers are added to or removed from the pool.
-        subscribe(poolEntity, BalanceableWorkerPool.CONTAINER_ADDED, eventHandler);
-        subscribe(poolEntity, BalanceableWorkerPool.CONTAINER_REMOVED, eventHandler);
-        subscribe(poolEntity, BalanceableWorkerPool.ITEM_ADDED, eventHandler);
-        subscribe(poolEntity, BalanceableWorkerPool.ITEM_REMOVED, eventHandler);
-        subscribe(poolEntity, BalanceableWorkerPool.ITEM_MOVED, eventHandler);
-        
-        // Take heed of any extant containers.
-        for (Entity container : poolEntity.getContainerGroup().getMembers()) {
-            onContainerAdded((NodeType)container, false);
-        }
-        for (Entity item : poolEntity.getItemGroup().getMembers()) {
-            onItemAdded((ItemType)item, (NodeType)item.getAttribute(Movable.CONTAINER), false);
-        }
-
-        scheduleRebalance();
-    }
-    
-    @Override
-    public void suspend() {
-        // TODO unsubscribe from everything? And resubscribe on resume?
-        super.suspend();
-        if (executor != null) executor.shutdownNow();;
-        executorQueued.set(false);
-    }
-    
-    @Override
-    public void resume() {
-        super.resume();
-        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
-        executorTime = 0;
-        executorQueued.set(false);
-    }
-    
-    private ThreadFactory newThreadFactory() {
-        return new ThreadFactoryBuilder()
-                .setNameFormat("brooklyn-followthesunpolicy-%d")
-                .build();
-    }
-
-    private void scheduleRebalance() {
-        if (isRunning() && executorQueued.compareAndSet(false, true)) {
-            long now = System.currentTimeMillis();
-            long delay = Math.max(0, (executorTime + minPeriodBetweenExecs) - now);
-            
-            executor.schedule(new Runnable() {
-                @SuppressWarnings("rawtypes")
-                public void run() {
-                    try {
-                        executorTime = System.currentTimeMillis();
-                        executorQueued.set(false);
-                        strategy.rebalance();
-
-                        if (LOG.isDebugEnabled()) LOG.debug("{} post-rebalance: poolSize={}; workrate={}; lowThreshold={}; " + 
-                                "highThreshold={}", new Object[] {this, model.getPoolSize(), model.getCurrentPoolWorkrate(), 
-                                model.getPoolLowThreshold(), model.getPoolHighThreshold()});
-                        
-                        if (model.isCold()) {
-                            Map eventVal = ImmutableMap.of(
-                                    AutoScalerPolicy.POOL_CURRENT_SIZE_KEY, model.getPoolSize(),
-                                    AutoScalerPolicy.POOL_CURRENT_WORKRATE_KEY, model.getCurrentPoolWorkrate(),
-                                    AutoScalerPolicy.POOL_LOW_THRESHOLD_KEY, model.getPoolLowThreshold(),
-                                    AutoScalerPolicy.POOL_HIGH_THRESHOLD_KEY, model.getPoolHighThreshold());
-            
-                            ((EntityLocal)poolEntity).emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, eventVal);
-                            
-                            if (LOG.isInfoEnabled()) {
-                                int desiredPoolSize = (int) Math.ceil(model.getCurrentPoolWorkrate() / (model.getPoolLowThreshold()/model.getPoolSize()));
-                                if (desiredPoolSize != lastEmittedDesiredPoolSize || lastEmittedPoolTemperature != TemperatureStates.COLD) {
-                                    LOG.info("{} emitted COLD (suggesting {}): {}", new Object[] {this, desiredPoolSize, eventVal});
-                                    lastEmittedDesiredPoolSize = desiredPoolSize;
-                                    lastEmittedPoolTemperature = TemperatureStates.COLD;
-                                }
-                            }
-                        
-                        } else if (model.isHot()) {
-                            Map eventVal = ImmutableMap.of(
-                                    AutoScalerPolicy.POOL_CURRENT_SIZE_KEY, model.getPoolSize(),
-                                    AutoScalerPolicy.POOL_CURRENT_WORKRATE_KEY, model.getCurrentPoolWorkrate(),
-                                    AutoScalerPolicy.POOL_LOW_THRESHOLD_KEY, model.getPoolLowThreshold(),
-                                    AutoScalerPolicy.POOL_HIGH_THRESHOLD_KEY, model.getPoolHighThreshold());
-                            
-                            ((EntityLocal)poolEntity).emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, eventVal);
-                            
-                            if (LOG.isInfoEnabled()) {
-                                int desiredPoolSize = (int) Math.ceil(model.getCurrentPoolWorkrate() / (model.getPoolHighThreshold()/model.getPoolSize()));
-                                if (desiredPoolSize != lastEmittedDesiredPoolSize || lastEmittedPoolTemperature != TemperatureStates.HOT) {
-                                    LOG.info("{} emitted HOT (suggesting {}): {}", new Object[] {this, desiredPoolSize, eventVal});
-                                    lastEmittedDesiredPoolSize = desiredPoolSize;
-                                    lastEmittedPoolTemperature = TemperatureStates.HOT;
-                                }
-                            }
-                        }
-
-                    } catch (Exception e) {
-                        if (isRunning()) {
-                            LOG.error("Error rebalancing", e);
-                        } else {
-                            LOG.debug("Error rebalancing, but no longer running", e);
-                        }
-                    }
-                }},
-                delay,
-                TimeUnit.MILLISECONDS);
-        }
-    }
-    
-    // TODO Can get duplicate onContainerAdded events.
-    //      I presume it's because we subscribe and then iterate over the extant containers.
-    //      Solution would be for subscription to give you events for existing / current value(s).
-    //      Also current impl messes up single-threaded updates model: the setEntity is a different thread than for subscription events.
-    private void onContainerAdded(NodeType newContainer, boolean rebalanceNow) {
-        Preconditions.checkArgument(newContainer instanceof BalanceableContainer, "Added container must be a BalanceableContainer");
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording addition of container {}", this, newContainer);
-        // Low and high thresholds for the metric we're interested in are assumed to be present
-        // in the container's configuration.
-        Number lowThreshold = (Number) findConfigValue(newContainer, lowThresholdConfigKeyName);
-        Number highThreshold = (Number) findConfigValue(newContainer, highThresholdConfigKeyName);
-        if (lowThreshold == null || highThreshold == null) {
-            LOG.warn(
-                "Balanceable container '"+newContainer+"' does not define low- and high- threshold configuration keys: '"+
-                lowThresholdConfigKeyName+"' and '"+highThresholdConfigKeyName+"', skipping");
-            return;
-        }
-        
-        model.onContainerAdded(newContainer, lowThreshold.doubleValue(), highThreshold.doubleValue());
-        
-        // Note: no need to scan the container for items; they will appear via the ITEM_ADDED events.
-        // Also, must abide by any item-filters etc defined in the pool.
-        
-        if (rebalanceNow) scheduleRebalance();
-    }
-    
-    private static Object findConfigValue(Entity entity, String configKeyName) {
-        Map<ConfigKey<?>, Object> config = ((EntityInternal)entity).getAllConfig();
-        for (Entry<ConfigKey<?>, Object> entry : config.entrySet()) {
-            if (configKeyName.equals(entry.getKey().getName()))
-                return entry.getValue();
-        }
-        return null;
-    }
-    
-    // TODO Receiving duplicates of onContainerRemoved (e.g. when running LoadBalancingInmemorySoakTest)
-    private void onContainerRemoved(NodeType oldContainer, boolean rebalanceNow) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording removal of container {}", this, oldContainer);
-        model.onContainerRemoved(oldContainer);
-        if (rebalanceNow) scheduleRebalance();
-    }
-    
-    private void onItemAdded(ItemType item, NodeType parentContainer, boolean rebalanceNow) {
-        Preconditions.checkArgument(item instanceof Movable, "Added item "+item+" must implement Movable");
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording addition of item {} in container {}", new Object[] {this, item, parentContainer});
-        
-        subscribe(item, metric, eventHandler);
-        
-        // Update the model, including the current metric value (if any).
-        boolean immovable = (Boolean)elvis(item.getConfig(Movable.IMMOVABLE), false);
-        Number currentValue = item.getAttribute(metric);
-        model.onItemAdded(item, parentContainer, immovable);
-        if (currentValue != null)
-            model.onItemWorkrateUpdated(item, currentValue.doubleValue());
-        
-        if (rebalanceNow) scheduleRebalance();
-    }
-    
-    private void onItemRemoved(ItemType item, NodeType parentContainer, boolean rebalanceNow) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording removal of item {}", this, item);
-        unsubscribe(item);
-        model.onItemRemoved(item);
-        if (rebalanceNow) scheduleRebalance();
-    }
-    
-    private void onItemMoved(ItemType item, NodeType parentContainer, boolean rebalanceNow) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording moving of item {} to {}", new Object[] {this, item, parentContainer});
-        model.onItemMoved(item, parentContainer);
-        if (rebalanceNow) scheduleRebalance();
-    }
-    
-    private void onItemMetricUpdate(ItemType item, double newValue, boolean rebalanceNow) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording metric update for item {}, new value {}", new Object[] {this, item, newValue});
-        model.onItemWorkrateUpdated(item, newValue);
-        if (rebalanceNow) scheduleRebalance();
-    }
-    
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + (groovyTruth(name) ? "("+name+")" : "");
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/LocationConstraint.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/LocationConstraint.java b/policy/src/main/java/brooklyn/policy/loadbalancing/LocationConstraint.java
deleted file mode 100644
index 5ae5723..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/LocationConstraint.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import org.apache.brooklyn.api.location.Location;
-
-/**
- * Temporary stub to resolve dependencies in ported LoadBalancingPolicy.
- */
-public class LocationConstraint {
-    public boolean isPermitted(Location l) { return true; } // TODO
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/Movable.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/Movable.java b/policy/src/main/java/brooklyn/policy/loadbalancing/Movable.java
deleted file mode 100644
index b0f1658..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/Movable.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.annotation.Effector;
-import brooklyn.entity.annotation.EffectorParam;
-import brooklyn.entity.basic.MethodEffector;
-import brooklyn.event.basic.BasicAttributeSensor;
-import brooklyn.event.basic.BasicConfigKey;
-
-
-/**
- * Represents an item that can be migrated between balanceable containers.
- */
-public interface Movable extends Entity {
-    
-    @SetFromFlag("immovable")
-    public static ConfigKey<Boolean> IMMOVABLE = new BasicConfigKey<Boolean>(
-        Boolean.class, "movable.item.immovable", "Indicates whether this item instance is immovable, so cannot be moved by policies", false);
-    
-    public static BasicAttributeSensor<BalanceableContainer> CONTAINER = new BasicAttributeSensor<BalanceableContainer>(
-        BalanceableContainer.class, "movable.item.container", "The container that this item is on");
-    
-    public static final MethodEffector<Void> MOVE = new MethodEffector<Void>(Movable.class, "move");
-    
-    public String getContainerId();
-    
-    @Effector(description="Moves this entity to the given container")
-    public void move(@EffectorParam(name="destination") Entity destination);
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/PolicyUtilForPool.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/PolicyUtilForPool.java b/policy/src/main/java/brooklyn/policy/loadbalancing/PolicyUtilForPool.java
deleted file mode 100644
index ceead1c..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/PolicyUtilForPool.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import java.util.Set;
-
-/**
- * Provides conveniences for searching for hot/cold containers in a provided pool model.
- * Ported from Monterey v3, with irrelevant bits removed.
- */
-public class PolicyUtilForPool<ContainerType, ItemType> {
-    
-    private final BalanceablePoolModel<ContainerType, ItemType> model;
-    
-    
-    public PolicyUtilForPool (BalanceablePoolModel<ContainerType, ItemType> model) {
-        this.model = model;
-    }
-    
-    public ContainerType findColdestContainer(Set<ContainerType> excludedContainers) {
-        return findColdestContainer(excludedContainers, null);
-    }
-    
-    /**
-     * Identifies the container with the maximum spare capacity (highThreshold - currentWorkrate),
-     * returns null if none of the model's nodes has spare capacity.
-     */
-    public ContainerType findColdestContainer(Set<ContainerType> excludedContainers, LocationConstraint locationConstraint) {
-        double maxSpareCapacity = 0;
-        ContainerType coldest = null;
-        
-        for (ContainerType c : model.getPoolContents()) {
-            if (excludedContainers.contains(c))
-                continue;
-            if (locationConstraint != null && !locationConstraint.isPermitted(model.getLocation(c)))
-                continue;
-            
-            double highThreshold = model.getHighThreshold(c);
-            double totalWorkrate = model.getTotalWorkrate(c);
-            double spareCapacity = highThreshold - totalWorkrate;
-            
-            if (highThreshold == -1 || totalWorkrate == -1) {
-                continue; // container presumably has been removed
-            }
-            if (spareCapacity > maxSpareCapacity) {
-                maxSpareCapacity = spareCapacity;
-                coldest = c;
-            }
-        }
-        return coldest;
-    }
-    
-    /**
-     * Identifies the container with the maximum overshoot (currentWorkrate - highThreshold),
-     * returns null if none of the model's  nodes has an overshoot.
-     */
-    public ContainerType findHottestContainer(Set<ContainerType> excludedContainers) {
-        double maxOvershoot = 0;
-        ContainerType hottest = null;
-        
-        for (ContainerType c : model.getPoolContents()) {
-            if (excludedContainers.contains(c))
-                continue;
-            
-            double totalWorkrate = model.getTotalWorkrate(c);
-            double highThreshold = model.getHighThreshold(c);
-            double overshoot = totalWorkrate - highThreshold;
-            
-            if (highThreshold == -1 || totalWorkrate == -1) {
-                continue; // container presumably has been removed
-            }
-            if (overshoot > maxOvershoot) {
-                maxOvershoot = overshoot;
-                hottest = c;
-            }
-        }
-        return hottest;
-    }
-    
-}
\ No newline at end of file


[19/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/autoscaling/SizeHistory.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/autoscaling/SizeHistory.java b/policy/src/main/java/brooklyn/policy/autoscaling/SizeHistory.java
deleted file mode 100644
index 525ba20..0000000
--- a/policy/src/main/java/brooklyn/policy/autoscaling/SizeHistory.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.autoscaling;
-
-import java.util.List;
-
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.collections.TimeWindowedList;
-import brooklyn.util.collections.TimestampedValue;
-import brooklyn.util.time.Duration;
-
-import com.google.common.base.Objects;
-
-/**
- * Using a {@link TimeWindowedList}, tracks the recent history of values to allow a summary of 
- * those values to be obtained. 
- *   
- * @author aled
- */
-public class SizeHistory {
-
-    public static class WindowSummary {
-        /** The most recent value (or -1 if there has been no value) */
-        public final long latest;
-        
-        /** The minimum vaule within the given time period */
-        public final long min;
-        
-        /** The maximum vaule within the given time period */
-        public final long max;
-        
-        /** true if, since that max value, there have not been any higher values */
-        public final boolean stableForGrowth;
-        
-        /** true if, since that low value, there have not been any lower values */
-        public final boolean stableForShrinking;
-        
-        public WindowSummary(long latest, long min, long max, boolean stableForGrowth, boolean stableForShrinking) {
-            this.latest = latest;
-            this.min = min;
-            this.max = max;
-            this.stableForGrowth = stableForGrowth;
-            this.stableForShrinking = stableForShrinking;
-        }
-        
-        @Override
-        public String toString() {
-            return Objects.toStringHelper(this).add("latest", latest).add("min", min).add("max", max)
-                    .add("stableForGrowth", stableForGrowth).add("stableForShrinking", stableForShrinking).toString();
-        }
-    }
-    
-    private final TimeWindowedList<Number> recentDesiredResizes;
-    
-    public SizeHistory(long windowSize) {
-        recentDesiredResizes = new TimeWindowedList<Number>(MutableMap.of("timePeriod", windowSize, "minExpiredVals", 1));
-    }
-
-    public void add(final int val) {
-        recentDesiredResizes.add(val);
-    }
-
-    public void setWindowSize(Duration newWindowSize) {
-        recentDesiredResizes.setTimePeriod(newWindowSize);
-    }
-    
-    /**
-     * Summarises the history of values in this time window, with a few special things:
-     * <ul>
-     *   <li>If entire time-window is not covered by the given values, then min is Integer.MIN_VALUE and max is Integer.MAX_VALUE 
-     *   <li>If no values, then latest is -1
-     *   <li>If no recent values, then keeps last-seen value (no matter how old), to use that
-     *   <li>"stable for growth" means that since that max value, there have not been any higher values
-     *   <li>"stable for shrinking" means that since that low value, there have not been any lower values
-     * </ul>
-     */
-    public WindowSummary summarizeWindow(Duration windowSize) {
-        long now = System.currentTimeMillis();
-        List<TimestampedValue<Number>> windowVals = recentDesiredResizes.getValuesInWindow(now, windowSize);
-        
-        Number latestObj = latestInWindow(windowVals);
-        long latest = (latestObj == null) ? -1: latestObj.longValue();
-        long max = maxInWindow(windowVals, windowSize).longValue();
-        long min = minInWindow(windowVals, windowSize).longValue();
-        
-        // TODO Could do more sophisticated "stable" check; this is the easiest code - correct but not most efficient
-        // in terms of the caller having to schedule additional stability checks.
-        boolean stable = (min == max);
-        
-        return new WindowSummary(latest, min, max, stable, stable);
-    }
-    
-    /**
-     * If the entire time-window is not covered by the given values, then returns Integer.MAX_VALUE.
-     */
-    private <T extends Number> T maxInWindow(List<TimestampedValue<T>> vals, Duration timeWindow) {
-        // TODO bad casting from Integer default result to T
-        long now = System.currentTimeMillis();
-        long epoch = now - timeWindow.toMilliseconds();
-        T result = null;
-        double resultAsDouble = Integer.MAX_VALUE;
-        for (TimestampedValue<T> val : vals) {
-            T valAsNum = val.getValue();
-            double valAsDouble = (valAsNum != null) ? valAsNum.doubleValue() : 0;
-            if (result == null && val.getTimestamp() > epoch) {
-                result = withDefault(null, Integer.MAX_VALUE);
-                resultAsDouble = result.doubleValue();
-            }
-            if (result == null || (valAsNum != null && valAsDouble > resultAsDouble)) {
-                result = valAsNum;
-                resultAsDouble = valAsDouble;
-            }
-        }
-        return withDefault(result, Integer.MAX_VALUE);
-    }
-    
-    /**
-     * If the entire time-window is not covered by the given values, then returns Integer.MIN_VALUE
-     */
-    private <T extends Number> T minInWindow(List<TimestampedValue<T>> vals, Duration timeWindow) {
-        long now = System.currentTimeMillis();
-        long epoch = now - timeWindow.toMilliseconds();
-        T result = null;
-        double resultAsDouble = Integer.MIN_VALUE;
-        for (TimestampedValue<T> val : vals) {
-            T valAsNum = val.getValue();
-            double valAsDouble = (valAsNum != null) ? valAsNum.doubleValue() : 0;
-            if (result == null && val.getTimestamp() > epoch) {
-                result = withDefault(null, Integer.MIN_VALUE);
-                resultAsDouble = result.doubleValue();
-            }
-            if (result == null || (val.getValue() != null && valAsDouble < resultAsDouble)) {
-                result = valAsNum;
-                resultAsDouble = valAsDouble;
-            }
-        }
-        return withDefault(result, Integer.MIN_VALUE);
-    }
-
-    @SuppressWarnings("unchecked")
-    private <T> T withDefault(T result, Integer defaultValue) {
-        return result!=null ? result : (T) defaultValue;
-    }
-    /**
-     * @return null if empty, or the most recent value
-     */
-    private <T extends Number> T latestInWindow(List<TimestampedValue<T>> vals) {
-        return vals.isEmpty() ? null : vals.get(vals.size()-1).getValue();
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/followthesun/DefaultFollowTheSunModel.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/followthesun/DefaultFollowTheSunModel.java b/policy/src/main/java/brooklyn/policy/followthesun/DefaultFollowTheSunModel.java
deleted file mode 100644
index 922b33d..0000000
--- a/policy/src/main/java/brooklyn/policy/followthesun/DefaultFollowTheSunModel.java
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import java.io.ByteArrayOutputStream;
-import java.io.PrintStream;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.location.basic.AbstractLocation;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Maps;
-
-public class DefaultFollowTheSunModel<ContainerType, ItemType> implements FollowTheSunModel<ContainerType, ItemType> {
-    
-    private static final Logger LOG = LoggerFactory.getLogger(DefaultFollowTheSunModel.class);
-    
-    // Concurrent maps cannot have null value; use this to represent when no container is supplied for an item 
-    private static final String NULL = "null-val";
-    private static final Location NULL_LOCATION = new AbstractLocation(newHashMap("name","null-location")) {};
-    
-    private final String name;
-    private final Set<ContainerType> containers = Collections.newSetFromMap(new ConcurrentHashMap<ContainerType,Boolean>());
-    private final Map<ItemType, ContainerType> itemToContainer = new ConcurrentHashMap<ItemType, ContainerType>();
-    private final Map<ContainerType, Location> containerToLocation = new ConcurrentHashMap<ContainerType, Location>();
-    private final Map<ItemType, Location> itemToLocation = new ConcurrentHashMap<ItemType, Location>();
-    private final Map<ItemType, Map<? extends ItemType, Double>> itemUsage = new ConcurrentHashMap<ItemType, Map<? extends ItemType,Double>>();
-    private final Set<ItemType> immovableItems = Collections.newSetFromMap(new ConcurrentHashMap<ItemType, Boolean>());
-
-    public DefaultFollowTheSunModel(String name) {
-        this.name = name;
-    }
-
-    @Override
-    public Set<ItemType> getItems() {
-        return itemToContainer.keySet();
-    }
-    
-    @Override
-    public ContainerType getItemContainer(ItemType item) {
-        ContainerType result = itemToContainer.get(item);
-        return (isNull(result) ? null : result);
-    }
-    
-    @Override
-    public Location getItemLocation(ItemType item) {
-        Location result = itemToLocation.get(item);
-        return (isNull(result) ? null : result);
-    }
-    
-    @Override
-    public Location getContainerLocation(ContainerType container) {
-        Location result = containerToLocation.get(container);
-        return (isNull(result) ? null : result);
-    }
-    
-    // Provider methods.
-    
-    @Override public String getName() {
-        return name;
-    }
-    
-    // TODO: delete?
-    @Override public String getName(ItemType item) {
-        return item.toString();
-    }
-    
-    @Override public boolean isItemMoveable(ItemType item) {
-        // If don't know about item, then assume not movable; otherwise has this item been explicitly flagged as immovable?
-        return hasItem(item) && !immovableItems.contains(item);
-    }
-    
-    @Override public boolean isItemAllowedIn(ItemType item, Location location) {
-        return true; // TODO?
-    }
-    
-    @Override public boolean hasActiveMigration(ItemType item) {
-        return false; // TODO?
-    }
-    
-    @Override
-    // FIXME Too expensive to compute; store in a different data structure?
-    public Map<ItemType, Map<Location, Double>> getDirectSendsToItemByLocation() {
-        Map<ItemType, Map<Location, Double>> result = new LinkedHashMap<ItemType, Map<Location,Double>>(getNumItems());
-        
-        for (Map.Entry<ItemType, Map<? extends ItemType, Double>> entry : itemUsage.entrySet()) {
-            ItemType targetItem = entry.getKey();
-            Map<? extends ItemType, Double> sources = entry.getValue();
-            if (sources.isEmpty()) continue; // no-one talking to us
-            
-            Map<Location, Double> targetUsageByLocation = new LinkedHashMap<Location, Double>();
-            result.put(targetItem, targetUsageByLocation);
-
-            for (Map.Entry<? extends ItemType, Double> entry2 : sources.entrySet()) {
-                ItemType sourceItem = entry2.getKey();
-                Location sourceLocation = getItemLocation(sourceItem);
-                double usageVal = (entry.getValue() != null) ? entry2.getValue() : 0d;
-                if (sourceLocation == null) continue; // don't know where to attribute this load; e.g. item may have just terminated
-                if (sourceItem.equals(targetItem)) continue; // ignore msgs to self
-                
-                Double usageValTotal = targetUsageByLocation.get(sourceLocation);
-                double newUsageValTotal = (usageValTotal != null ? usageValTotal : 0d) + usageVal;
-                targetUsageByLocation.put(sourceLocation, newUsageValTotal);
-            }
-        }
-        
-        return result;
-    }
-    
-    @Override
-    public Set<ContainerType> getAvailableContainersFor(ItemType item, Location location) {
-        checkNotNull(location);
-        return getContainersInLocation(location);
-    }
-
-
-    // Mutators.
-    
-    @Override
-    public void onItemMoved(ItemType item, ContainerType newContainer) {
-        // idempotent, as may be called multiple times
-        Location newLocation = (newContainer != null) ? containerToLocation.get(newContainer) : null;
-        ContainerType newContainerNonNull = toNonNullContainer(newContainer);
-        Location newLocationNonNull = toNonNullLocation(newLocation);
-        ContainerType oldContainer = itemToContainer.put(item, newContainerNonNull);
-        Location oldLocation = itemToLocation.put(item, newLocationNonNull);
-    }
-    
-    @Override
-    public void onContainerAdded(ContainerType container, Location location) {
-        Location locationNonNull = toNonNullLocation(location);
-        containers.add(container);
-        containerToLocation.put(container, locationNonNull);
-        for (ItemType item : getItemsOnContainer(container)) {
-            itemToLocation.put(item, locationNonNull);
-        }
-    }
-    
-    @Override
-    public void onContainerRemoved(ContainerType container) {
-        containers.remove(container);
-        containerToLocation.remove(container);
-    }
-    
-    public void onContainerLocationUpdated(ContainerType container, Location location) {
-        if (!containers.contains(container)) {
-            // unknown container; probably just stopped? 
-            // If this overtook onContainerAdded, then assume we'll lookup the location and get it right in onContainerAdded
-            if (LOG.isDebugEnabled()) LOG.debug("Ignoring setting of location for unknown container {}, to {}", container, location);
-            return;
-        }
-        Location locationNonNull = toNonNullLocation(location);
-        containerToLocation.put(container, locationNonNull);
-        for (ItemType item : getItemsOnContainer(container)) {
-            itemToLocation.put(item, locationNonNull);
-        }
-    }
-
-    @Override
-    public void onItemAdded(ItemType item, ContainerType container, boolean immovable) {
-        // idempotent, as may be called multiple times
-        
-        if (immovable) {
-            immovableItems.add(item);
-        }
-        Location location = (container != null) ? containerToLocation.get(container) : null;
-        ContainerType containerNonNull = toNonNullContainer(container);
-        Location locationNonNull = toNonNullLocation(location);
-        ContainerType oldContainer = itemToContainer.put(item, containerNonNull);
-        Location oldLocation = itemToLocation.put(item, locationNonNull);
-    }
-    
-    @Override
-    public void onItemRemoved(ItemType item) {
-        itemToContainer.remove(item);
-        itemToLocation.remove(item);
-        itemUsage.remove(item);
-        immovableItems.remove(item);
-    }
-    
-    @Override
-    public void onItemUsageUpdated(ItemType item, Map<? extends ItemType, Double> newValue) {
-        if (hasItem(item)) {
-            itemUsage.put(item, newValue);
-        } else {
-            // Can happen when item removed - get notification of removal and workrate from group and item
-            // respectively, so can overtake each other
-            if (LOG.isDebugEnabled()) LOG.debug("Ignoring setting of usage for unknown item {}, to {}", item, newValue);
-        }
-    }
-    
-    
-    // Additional methods for tests.
-
-    /**
-     * Warning: this can be an expensive (time and memory) operation if there are a lot of items/containers. 
-     */
-    @VisibleForTesting
-    public String itemDistributionToString() {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        dumpItemDistribution(new PrintStream(baos));
-        return new String(baos.toByteArray());
-    }
-
-    @VisibleForTesting
-    public void dumpItemDistribution() {
-        dumpItemDistribution(System.out);
-    }
-    
-    @VisibleForTesting
-    public void dumpItemDistribution(PrintStream out) {
-        Map<ItemType, Map<Location, Double>> directSendsToItemByLocation = getDirectSendsToItemByLocation();
-        
-        out.println("Follow-The-Sun dump: ");
-        for (Location location: getLocations()) {
-            out.println("\t"+"Location "+location);
-            for (ContainerType container : getContainersInLocation(location)) {
-                out.println("\t\t"+"Container "+container);
-                for (ItemType item : getItemsOnContainer(container)) {
-                    Map<Location, Double> inboundUsage = directSendsToItemByLocation.get(item);
-                    Map<? extends ItemType, Double> outboundUsage = itemUsage.get(item);
-                    double totalInboundByLocation = (inboundUsage != null) ? sum(inboundUsage.values()) : 0d;
-                    double totalInboundByActor = (outboundUsage != null) ? sum(outboundUsage.values()) : 0d;
-                    out.println("\t\t\t"+"Item "+item);
-                    out.println("\t\t\t\t"+"Inbound-by-location: "+totalInboundByLocation+": "+inboundUsage);
-                    out.println("\t\t\t\t"+"Inbound-by-actor: "+totalInboundByActor+": "+outboundUsage);
-                }
-            }
-        }
-        out.flush();
-    }
-    
-    private boolean hasItem(ItemType item) {
-        return itemToContainer.containsKey(item);
-    }
-    
-    private Set<Location> getLocations() {
-        return ImmutableSet.copyOf(containerToLocation.values());
-    }
-    
-    private Set<ContainerType> getContainersInLocation(Location location) {
-        Set<ContainerType> result = new LinkedHashSet<ContainerType>();
-        for (Map.Entry<ContainerType, Location> entry : containerToLocation.entrySet()) {
-            if (location.equals(entry.getValue())) {
-                result.add(entry.getKey());
-            }
-        }
-        return result;
-    }
-    
-    private Set<ItemType> getItemsOnContainer(ContainerType container) {
-        Set<ItemType> result = new LinkedHashSet<ItemType>();
-        for (Map.Entry<ItemType, ContainerType> entry : itemToContainer.entrySet()) {
-            if (container.equals(entry.getValue())) {
-                result.add(entry.getKey());
-            }
-        }
-        return result;
-    }
-    
-    private int getNumItems() {
-        return itemToContainer.size();
-    }
-    
-    @SuppressWarnings("unchecked")
-    private ContainerType nullContainer() {
-        return (ContainerType) NULL; // relies on erasure
-    }
-    
-    private Location nullLocation() {
-        return NULL_LOCATION;
-    }
-    
-    private ContainerType toNonNullContainer(ContainerType val) {
-        return (val != null) ? val : nullContainer();
-    }
-    
-    private Location toNonNullLocation(Location val) {
-        return (val != null) ? val : nullLocation();
-    }
-    
-    private boolean isNull(Object val) {
-        return val == NULL || val == NULL_LOCATION;
-    }
-    
-    // TODO Move to utils; or stop AbstractLocation from removing things from the map!
-    public static <K,V> Map<K,V> newHashMap(K k, V v) {
-        Map<K,V> result = Maps.newLinkedHashMap();
-        result.put(k, v);
-        return result;
-    }
-    
-    public static double sum(Collection<? extends Number> values) {
-        double total = 0;
-        for (Number d : values) {
-            total += d.doubleValue();
-        }
-        return total;
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunModel.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunModel.java b/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunModel.java
deleted file mode 100644
index a181ae4..0000000
--- a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunModel.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import java.util.Map;
-import java.util.Set;
-
-import org.apache.brooklyn.api.location.Location;
-
-/**
- * Captures the state of items, containers and locations for the purpose of moving items around
- * to minimise latency. For consumption by a {@link FollowTheSunStrategy}.
- */
-public interface FollowTheSunModel<ContainerType, ItemType> {
-
-    // Attributes of the pool.
-    public String getName();
-    
-    // Attributes of containers and items.
-    public String getName(ItemType item);
-    public Set<ItemType> getItems();
-    public Map<ItemType, Map<Location, Double>> getDirectSendsToItemByLocation();
-    public Location getItemLocation(ItemType item);
-    public ContainerType getItemContainer(ItemType item);
-    public Location getContainerLocation(ContainerType container);
-    public boolean hasActiveMigration(ItemType item);
-    public Set<ContainerType> getAvailableContainersFor(ItemType item, Location location);
-    public boolean isItemMoveable(ItemType item);
-    public boolean isItemAllowedIn(ItemType item, Location location);
-    
-    // Mutators for keeping the model in-sync with the observed world
-    public void onContainerAdded(ContainerType container, Location location);
-    public void onContainerRemoved(ContainerType container);
-    public void onContainerLocationUpdated(ContainerType container, Location location);
-
-    public void onItemAdded(ItemType item, ContainerType parentContainer, boolean immovable);
-    public void onItemRemoved(ItemType item);
-    public void onItemUsageUpdated(ItemType item, Map<? extends ItemType, Double> newValues);
-    public void onItemMoved(ItemType item, ContainerType newContainer);
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunParameters.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunParameters.java b/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunParameters.java
deleted file mode 100644
index 2b2d9af..0000000
--- a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunParameters.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import java.util.LinkedHashSet;
-import java.util.Set;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.brooklyn.api.location.Location;
-
-public class FollowTheSunParameters {
-
-    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunParameters.class);
-
-    private FollowTheSunParameters() {}
-
-    /** trigger for moving segment X from geo A to geo B:
-     * where x is total number of requests submitted in X across the CDM network,
-     * and x_A is number of reqs from geo A, with A the most prolific geography
-     * (arbitrarily chosen in case of ties so recommended to choose at least a small percent_majority or delta_above_percent_majority, in addition to this field);
-     * this parameter T defines a number such that x_A > T*x in order for X to be migrated to A
-     * (but see also DELTA_ABOVE_PERCENT_TOTAL, below) */
-    public double triggerPercentTotal = 0.3;
-    /** fields as above, and T as above,
-     * this parameter T' defines a number such that x_A > T*x + T' in order for X to be migrated to A */
-    public double triggerDeltaAbovePercentTotal = 0;
-    /** fields as above,
-     * this parameter T defines a number such that x_A > T in order for X to be migrated to A */
-    public double triggerAbsoluteTotal = 2;
-
-    /** fields as above, with X_B the number from a different geography B,
-     * where A and B are the two most prolific requesters of X, and X_A >= X_B;
-     * this parameter T defines a number such that x_A-x_B > T*x in order for X to be migrated to A */
-    public double triggerPercentMajority = 0.2;
-    /** as corresponding majority and total fields, with x_A-x_B on the LHS of inequality */
-    public double triggerDeltaAbovePercentMajority = 1;
-    /** as corresponding majority and total fields, with x_A-x_B on the LHS of inequality */
-    public double triggerAbsoluteMajority = 4;
-    
-    /** a list of excluded locations */
-    public Set<Location> excludedLocations = new LinkedHashSet<Location>();
-
-    public static FollowTheSunParameters newDefault() {
-        return new FollowTheSunParameters();
-    }
-
-    private static double parseDouble(String text, double defaultValue) {
-        try {
-            double d = Double.parseDouble(text);
-            if (!Double.isNaN(d)) return d;
-        } catch (Exception e) {
-            LOG.warn("Illegal double value '"+text+"', using default "+defaultValue+": "+e, e);
-        }
-        return defaultValue;
-    }
-
-    private static String[] parseCommaSeparatedList(String csv) {
-        if (csv==null || csv.trim().length()==0) return new String[0];
-        return csv.split(",");
-    }
-
-    public boolean isTriggered(double highest, double total, double nextHighest, double current) {
-        if (highest <= current) return false;
-        if (highest < total*triggerPercentTotal + triggerDeltaAbovePercentTotal) return false;
-        if (highest < triggerAbsoluteTotal) return false;
-        //TODO more params about nextHighest vs current
-        if (highest-current < total*triggerPercentMajority + triggerDeltaAbovePercentMajority) return false;
-        if (highest-current < triggerAbsoluteMajority) return false;
-        return true;
-    }
-    
-    public String toString() {
-        return "Inter-geography policy params: percentTotal="+triggerPercentTotal+"; deltaAbovePercentTotal="+triggerDeltaAbovePercentTotal+
-                "; absoluteTotal="+triggerAbsoluteTotal+"; percentMajority="+triggerPercentMajority+
-                "; deltaAbovePercentMajority="+triggerDeltaAbovePercentMajority+"; absoluteMajority="+triggerAbsoluteMajority;
-
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPolicy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPolicy.java b/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPolicy.java
deleted file mode 100644
index b76682c..0000000
--- a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPolicy.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import static brooklyn.util.JavaGroovyEquivalents.elvis;
-import static brooklyn.util.JavaGroovyEquivalents.groovyTruth;
-import static com.google.common.base.Preconditions.checkArgument;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.event.AttributeSensor;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.api.location.MachineProvisioningLocation;
-import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.entity.basic.Attributes;
-import brooklyn.policy.followthesun.FollowTheSunPool.ContainerItemPair;
-import brooklyn.policy.loadbalancing.Movable;
-import brooklyn.util.collections.MutableMap;
-
-import com.google.common.base.Function;
-import com.google.common.collect.Iterables;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-
-    // removed from catalog because it cannot currently be configured via catalog mechanisms - 
-    // PolicySpec.create fails due to no no-arg constructor
-    // TODO make model and parameters things which can be initialized from config then reinstate in catalog
-//@Catalog(name="Follow the Sun", description="Policy for moving \"work\" around to follow the demand; "
-//        + "the work can be any \"Movable\" entity")
-public class FollowTheSunPolicy extends AbstractPolicy {
-
-    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunPolicy.class);
-
-    public static final String NAME = "Follow the Sun (Inter-Geography Latency Optimization)";
-
-    @SetFromFlag(defaultVal="100")
-    private long minPeriodBetweenExecs;
-    
-    @SetFromFlag
-    private Function<Entity, Location> locationFinder;
-    
-    private final AttributeSensor<Map<? extends Movable, Double>> itemUsageMetric;
-    private final FollowTheSunModel<Entity, Movable> model;
-    private final FollowTheSunStrategy<Entity, Movable> strategy;
-    private final FollowTheSunParameters parameters;
-    
-    private FollowTheSunPool poolEntity;
-    
-    private volatile ScheduledExecutorService executor;
-    private final AtomicBoolean executorQueued = new AtomicBoolean(false);
-    private volatile long executorTime = 0;
-    private boolean loggedConstraintsIgnored = false;
-    
-    private final Function<Entity, Location> defaultLocationFinder = new Function<Entity, Location>() {
-        public Location apply(Entity e) {
-            Collection<Location> locs = e.getLocations();
-            if (locs.isEmpty()) return null;
-            Location contender = Iterables.get(locs, 0);
-            while (contender.getParent() != null && !(contender instanceof MachineProvisioningLocation)) {
-                contender = contender.getParent();
-            }
-            return contender;
-        }
-    };
-    
-    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
-        @Override
-        public void onEvent(SensorEvent<Object> event) {
-            if (LOG.isTraceEnabled()) LOG.trace("{} received event {}", FollowTheSunPolicy.this, event);
-            Entity source = event.getSource();
-            Object value = event.getValue();
-            Sensor<?> sensor = event.getSensor();
-            
-            if (sensor.equals(itemUsageMetric)) {
-                onItemMetricUpdated((Movable)source, (Map<? extends Movable, Double>) value, true);
-            } else if (sensor.equals(Attributes.LOCATION_CHANGED)) {
-                onContainerLocationUpdated(source, true);
-            } else if (sensor.equals(FollowTheSunPool.CONTAINER_ADDED)) {
-                onContainerAdded((Entity) value, true);
-            } else if (sensor.equals(FollowTheSunPool.CONTAINER_REMOVED)) {
-                onContainerRemoved((Entity) value, true);
-            } else if (sensor.equals(FollowTheSunPool.ITEM_ADDED)) {
-                onItemAdded((Movable) value, true);
-            } else if (sensor.equals(FollowTheSunPool.ITEM_REMOVED)) {
-                onItemRemoved((Movable) value, true);
-            } else if (sensor.equals(FollowTheSunPool.ITEM_MOVED)) {
-                ContainerItemPair pair = (ContainerItemPair) value;
-                onItemMoved((Movable)pair.item, pair.container, true);
-            }
-        }
-    };
-    
-    // FIXME parameters: use a more groovy way of doing it, that's consistent with other policies/entities?
-    public FollowTheSunPolicy(AttributeSensor itemUsageMetric, 
-            FollowTheSunModel<Entity, Movable> model, FollowTheSunParameters parameters) {
-        this(MutableMap.of(), itemUsageMetric, model, parameters);
-    }
-    
-    public FollowTheSunPolicy(Map props, AttributeSensor itemUsageMetric, 
-            FollowTheSunModel<Entity, Movable> model, FollowTheSunParameters parameters) {
-        super(props);
-        this.itemUsageMetric = itemUsageMetric;
-        this.model = model;
-        this.parameters = parameters;
-        this.strategy = new FollowTheSunStrategy<Entity, Movable>(model, parameters); // TODO: extract interface, inject impl
-        this.locationFinder = elvis(locationFinder, defaultLocationFinder);
-        
-        // TODO Should re-use the execution manager's thread pool, somehow
-        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
-    }
-    
-    @Override
-    public void setEntity(EntityLocal entity) {
-        checkArgument(entity instanceof FollowTheSunPool, "Provided entity must be a FollowTheSunPool");
-        super.setEntity(entity);
-        this.poolEntity = (FollowTheSunPool) entity;
-        
-        // Detect when containers are added to or removed from the pool.
-        subscribe(poolEntity, FollowTheSunPool.CONTAINER_ADDED, eventHandler);
-        subscribe(poolEntity, FollowTheSunPool.CONTAINER_REMOVED, eventHandler);
-        subscribe(poolEntity, FollowTheSunPool.ITEM_ADDED, eventHandler);
-        subscribe(poolEntity, FollowTheSunPool.ITEM_REMOVED, eventHandler);
-        subscribe(poolEntity, FollowTheSunPool.ITEM_MOVED, eventHandler);
-        
-        // Take heed of any extant containers.
-        for (Entity container : poolEntity.getContainerGroup().getMembers()) {
-            onContainerAdded(container, false);
-        }
-        for (Entity item : poolEntity.getItemGroup().getMembers()) {
-            onItemAdded((Movable)item, false);
-        }
-
-        scheduleLatencyReductionJig();
-    }
-    
-    @Override
-    public void suspend() {
-        // TODO unsubscribe from everything? And resubscribe on resume?
-        super.suspend();
-        if (executor != null) executor.shutdownNow();
-        executorQueued.set(false);
-    }
-    
-    @Override
-    public void resume() {
-        super.resume();
-        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
-        executorTime = 0;
-        executorQueued.set(false);
-    }
-    
-    private ThreadFactory newThreadFactory() {
-        return new ThreadFactoryBuilder()
-                .setNameFormat("brooklyn-followthesunpolicy-%d")
-                .build();
-    }
-
-    private void scheduleLatencyReductionJig() {
-        if (isRunning() && executorQueued.compareAndSet(false, true)) {
-            long now = System.currentTimeMillis();
-            long delay = Math.max(0, (executorTime + minPeriodBetweenExecs) - now);
-            
-            executor.schedule(new Runnable() {
-                public void run() {
-                    try {
-                        executorTime = System.currentTimeMillis();
-                        executorQueued.set(false);
-                        
-                        if (LOG.isTraceEnabled()) LOG.trace("{} executing follow-the-sun migration-strategy", this);
-                        strategy.rebalance();
-                        
-                    } catch (RuntimeException e) {
-                        if (isRunning()) {
-                            LOG.error("Error during latency-reduction-jig", e);
-                        } else {
-                            LOG.debug("Error during latency-reduction-jig, but no longer running", e);
-                        }
-                    }
-                }},
-                delay,
-                TimeUnit.MILLISECONDS);
-        }
-    }
-    
-    private void onContainerAdded(Entity container, boolean rebalanceNow) {
-        subscribe(container, Attributes.LOCATION_CHANGED, eventHandler);
-        Location location = locationFinder.apply(container);
-        
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording addition of container {} in location {}", new Object[] {this, container, location});
-        model.onContainerAdded(container, location);
-        
-        if (rebalanceNow) scheduleLatencyReductionJig();
-    }
-    
-    private void onContainerRemoved(Entity container, boolean rebalanceNow) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording removal of container {}", this, container);
-        model.onContainerRemoved(container);
-        if (rebalanceNow) scheduleLatencyReductionJig();
-    }
-    
-    private void onItemAdded(Movable item, boolean rebalanceNow) {
-        Entity parentContainer = (Entity) item.getAttribute(Movable.CONTAINER);
-        
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording addition of item {} in container {}", new Object[] {this, item, parentContainer});
-        
-        subscribe(item, itemUsageMetric, eventHandler);
-        
-        // Update the model, including the current metric value (if any).
-        Map<? extends Movable, Double> currentValue = item.getAttribute(itemUsageMetric);
-        boolean immovable = (Boolean)elvis(item.getConfig(Movable.IMMOVABLE), false);
-        model.onItemAdded(item, parentContainer, immovable);
-
-        if (currentValue != null) {
-            model.onItemUsageUpdated(item, currentValue);
-        }
-        
-        if (rebalanceNow) scheduleLatencyReductionJig();
-    }
-    
-    private void onItemRemoved(Movable item, boolean rebalanceNow) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording removal of item {}", this, item);
-        unsubscribe(item);
-        model.onItemRemoved(item);
-        if (rebalanceNow) scheduleLatencyReductionJig();
-    }
-    
-    private void onItemMoved(Movable item, Entity parentContainer, boolean rebalanceNow) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording moving of item {} to {}", new Object[] {this, item, parentContainer});
-        model.onItemMoved(item, parentContainer);
-        if (rebalanceNow) scheduleLatencyReductionJig();
-    }
-    
-    private void onContainerLocationUpdated(Entity container, boolean rebalanceNow) {
-        Location location = locationFinder.apply(container);
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording location for container {}, new value {}", new Object[] {this, container, location});
-        model.onContainerLocationUpdated(container, location);
-        if (rebalanceNow) scheduleLatencyReductionJig();
-    }
-    
-    private void onItemMetricUpdated(Movable item, Map<? extends Movable, Double> newValues, boolean rebalanceNow) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording usage update for item {}, new value {}", new Object[] {this, item, newValues});
-        model.onItemUsageUpdated(item, newValues);
-        if (rebalanceNow) scheduleLatencyReductionJig();
-    }
-    
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + (groovyTruth(name) ? "("+name+")" : "");
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPool.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPool.java b/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPool.java
deleted file mode 100644
index 402eeef..0000000
--- a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPool.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import java.io.Serializable;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
-
-import brooklyn.entity.trait.Resizable;
-import brooklyn.event.basic.BasicNotificationSensor;
-
-@ImplementedBy(FollowTheSunPoolImpl.class)
-public interface FollowTheSunPool extends Entity, Resizable {
-
-    // FIXME Remove duplication from BalanceableWorkerPool?
-
-    // FIXME Asymmetry between loadbalancing and followTheSun: ITEM_ADDED and ITEM_REMOVED in loadbalancing
-    // are of type ContainerItemPair, but in followTheSun it is just the `Entity item`.
-    
-    /** Encapsulates an item and a container; emitted by sensors.
-     */
-    public static class ContainerItemPair implements Serializable {
-        private static final long serialVersionUID = 1L;
-        public final Entity container;
-        public final Entity item;
-
-        public ContainerItemPair(Entity container, Entity item) {
-            this.container = container;
-            this.item = checkNotNull(item);
-        }
-
-        @Override
-        public String toString() {
-            return ""+item+" @ "+container;
-        }
-    }
-
-    // Pool constituent notifications.
-    public static BasicNotificationSensor<Entity> CONTAINER_ADDED = new BasicNotificationSensor<Entity>(
-            Entity.class, "followthesun.container.added", "Container added");
-    public static BasicNotificationSensor<Entity> CONTAINER_REMOVED = new BasicNotificationSensor<Entity>(
-            Entity.class, "followthesun.container.removed", "Container removed");
-    public static BasicNotificationSensor<Entity> ITEM_ADDED = new BasicNotificationSensor<Entity>(
-            Entity.class, "followthesun.item.added", "Item added");
-    public static BasicNotificationSensor<Entity> ITEM_REMOVED = new BasicNotificationSensor<Entity>(
-            Entity.class, "followthesun.item.removed", "Item removed");
-    public static BasicNotificationSensor<ContainerItemPair> ITEM_MOVED = new BasicNotificationSensor<ContainerItemPair>(
-            ContainerItemPair.class, "followthesun.item.moved", "Item moved to the given container");
-
-    public void setContents(Group containerGroup, Group itemGroup);
-
-    public Group getContainerGroup();
-
-    public Group getItemGroup();
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPoolImpl.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPoolImpl.java b/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPoolImpl.java
deleted file mode 100644
index 4d74441..0000000
--- a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunPoolImpl.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.entity.basic.AbstractEntity;
-import brooklyn.entity.basic.AbstractGroup;
-import brooklyn.entity.trait.Resizable;
-import brooklyn.entity.trait.Startable;
-import brooklyn.policy.loadbalancing.Movable;
-
-public class FollowTheSunPoolImpl extends AbstractEntity implements FollowTheSunPool {
-
-    // FIXME Remove duplication from BalanceableWorkerPool?
-
-    // FIXME Asymmetry between loadbalancing and followTheSun: ITEM_ADDED and ITEM_REMOVED in loadbalancing
-    // are of type ContainerItemPair, but in followTheSun it is just the `Entity item`.
-    
-    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunPool.class);
-
-    private Group containerGroup;
-    private Group itemGroup;
-
-    private final Set<Entity> containers = Collections.synchronizedSet(new HashSet<Entity>());
-    private final Set<Entity> items = Collections.synchronizedSet(new HashSet<Entity>());
-
-    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
-        @Override
-        public void onEvent(SensorEvent<Object> event) {
-            if (LOG.isTraceEnabled()) LOG.trace("{} received event {}", FollowTheSunPoolImpl.this, event);
-            Entity source = event.getSource();
-            Object value = event.getValue();
-            Sensor sensor = event.getSensor();
-
-            if (sensor.equals(AbstractGroup.MEMBER_ADDED)) {
-                if (source.equals(containerGroup)) {
-                    onContainerAdded((Entity) value);
-                } else if (source.equals(itemGroup)) {
-                    onItemAdded((Entity)value);
-                } else {
-                    throw new IllegalStateException("unexpected event source="+source);
-                }
-            } else if (sensor.equals(AbstractGroup.MEMBER_REMOVED)) {
-                if (source.equals(containerGroup)) {
-                    onContainerRemoved((Entity) value);
-                } else if (source.equals(itemGroup)) {
-                    onItemRemoved((Entity) value);
-                } else {
-                    throw new IllegalStateException("unexpected event source="+source);
-                }
-            } else if (sensor.equals(Startable.SERVICE_UP)) {
-                // TODO What if start has failed? Is there a sensor to indicate that?
-                if ((Boolean)value) {
-                    onContainerUp(source);
-                } else {
-                    onContainerDown(source);
-                }
-            } else if (sensor.equals(Movable.CONTAINER)) {
-                onItemMoved(source, (Entity) value);
-            } else {
-                throw new IllegalStateException("Unhandled event type "+sensor+": "+event);
-            }
-        }
-    };
-
-    public FollowTheSunPoolImpl() {
-    }
-
-    @Override
-    public void setContents(Group containerGroup, Group itemGroup) {
-        this.containerGroup = containerGroup;
-        this.itemGroup = itemGroup;
-        subscribe(containerGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
-        subscribe(containerGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
-        subscribe(itemGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
-        subscribe(itemGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
-
-        // Process extant containers and items
-        for (Entity existingContainer : containerGroup.getMembers()) {
-            onContainerAdded(existingContainer);
-        }
-        for (Entity existingItem : itemGroup.getMembers()) {
-            onItemAdded((Entity)existingItem);
-        }
-    }
-
-    @Override
-    public Group getContainerGroup() {
-        return containerGroup;
-    }
-
-    @Override
-    public Group getItemGroup() {
-        return itemGroup;
-    }
-
-    @Override
-    public Integer getCurrentSize() {
-        return containerGroup.getCurrentSize();
-    }
-
-    @Override
-    public Integer resize(Integer desiredSize) {
-        if (containerGroup instanceof Resizable) return ((Resizable) containerGroup).resize(desiredSize);
-
-        throw new UnsupportedOperationException("Container group is not resizable");
-    }
-
-
-    private void onContainerAdded(Entity newContainer) {
-        subscribe(newContainer, Startable.SERVICE_UP, eventHandler);
-        if (!(newContainer instanceof Startable) || Boolean.TRUE.equals(newContainer.getAttribute(Startable.SERVICE_UP))) {
-            onContainerUp(newContainer);
-        }
-    }
-
-    private void onContainerUp(Entity newContainer) {
-        if (containers.add(newContainer)) {
-            emit(CONTAINER_ADDED, newContainer);
-        }
-    }
-
-    private void onContainerDown(Entity oldContainer) {
-        if (containers.remove(oldContainer)) {
-            emit(CONTAINER_REMOVED, oldContainer);
-        }
-    }
-
-    private void onContainerRemoved(Entity oldContainer) {
-        unsubscribe(oldContainer);
-        onContainerDown(oldContainer);
-    }
-
-    private void onItemAdded(Entity item) {
-        if (items.add(item)) {
-            subscribe(item, Movable.CONTAINER, eventHandler);
-            emit(ITEM_ADDED, item);
-        }
-    }
-
-    private void onItemRemoved(Entity item) {
-        if (items.remove(item)) {
-            unsubscribe(item);
-            emit(ITEM_REMOVED, item);
-        }
-    }
-
-    private void onItemMoved(Entity item, Entity container) {
-        emit(ITEM_MOVED, new ContainerItemPair(container, item));
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunStrategy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunStrategy.java b/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunStrategy.java
deleted file mode 100644
index e8ca636..0000000
--- a/policy/src/main/java/brooklyn/policy/followthesun/FollowTheSunStrategy.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.location.Location;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.policy.loadbalancing.Movable;
-
-import com.google.common.collect.Iterables;
-
-// TODO: extract interface
-public class FollowTheSunStrategy<ContainerType extends Entity, ItemType extends Movable> {
-    
-    // This is a modified version of the InterGeographyLatencyPolicy (aka Follow-The-Sun) policy from Monterey v3.
-    
-    // TODO location constraints
-    
-    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunStrategy.class);
-    
-    private final FollowTheSunParameters parameters;
-    private final FollowTheSunModel<ContainerType,ItemType> model;
-    private final String name;
-    
-    public FollowTheSunStrategy(FollowTheSunModel<ContainerType,ItemType> model, FollowTheSunParameters parameters) {
-        this.model = model;
-        this.parameters = parameters;
-        this.name = model.getName();
-    }
-    
-    public void rebalance() {
-        try {
-            Set<ItemType> items = model.getItems();
-            Map<ItemType, Map<Location, Double>> directSendsToItemByLocation = model.getDirectSendsToItemByLocation();
-            
-            for (ItemType item : items) {
-                String itemName = model.getName(item);
-                Location activeLocation = model.getItemLocation(item);
-                ContainerType activeContainer = model.getItemContainer(item);
-                Map<Location, Double> sendsByLocation = directSendsToItemByLocation.get(item);
-                if (sendsByLocation == null) sendsByLocation = Collections.emptyMap();
-                
-                if (parameters.excludedLocations.contains(activeLocation)) {
-                    if (LOG.isTraceEnabled()) LOG.trace("Ignoring segment {} as it is in {}", itemName, activeLocation);
-                    continue;
-                }
-                if (!model.isItemMoveable(item)) {
-                    if (LOG.isDebugEnabled()) LOG.debug("POLICY {} skipping any migration of {}, it is not moveable", name, itemName);
-                    continue;
-                }
-                if (model.hasActiveMigration(item)) {
-                    LOG.info("POLICY {} skipping any migration of {}, it is involved in an active migration already", name, itemName);
-                    continue;
-                }
-                
-                double total = DefaultFollowTheSunModel.sum(sendsByLocation.values());
-
-                if (LOG.isTraceEnabled()) LOG.trace("POLICY {} detected {} msgs/sec in {}, split up as: {}", new Object[] {name, total, itemName, sendsByLocation});
-                
-                Double current = sendsByLocation.get(activeLocation);
-                if (current == null) current=0d;
-                List<WeightedObject<Location>> locationsWtd = new ArrayList<WeightedObject<Location>>();
-                if (total > 0) {
-                    for (Map.Entry<Location, Double> entry : sendsByLocation.entrySet()) {
-                        Location l = entry.getKey();
-                        Double d = entry.getValue();
-                        if (d > current) locationsWtd.add(new WeightedObject<Location>(l, d));
-                    }
-                }
-                Collections.sort(locationsWtd);
-                Collections.reverse(locationsWtd);
-                
-                double highestMsgRate = -1;
-                Location highestLocation = null;
-                ContainerType optimalContainerInHighest = null;
-                while (!locationsWtd.isEmpty()) {
-                    WeightedObject<Location> weightedObject = locationsWtd.remove(0);
-                    highestMsgRate = weightedObject.getWeight();
-                    highestLocation = weightedObject.getObject();
-                    optimalContainerInHighest = findOptimal(model.getAvailableContainersFor(item, highestLocation));
-                    if (optimalContainerInHighest != null) {
-                        break;
-                    }
-                }
-                if (optimalContainerInHighest == null) {
-                    if (LOG.isDebugEnabled()) LOG.debug("POLICY {} detected {} is already in optimal permitted location ({} of {} msgs/sec)", new Object[] {name, itemName, highestMsgRate, total});
-                    continue;                   
-                }
-                
-                double nextHighestMsgRate = -1;
-                ContainerType optimalContainerInNextHighest = null;
-                while (!locationsWtd.isEmpty()) {
-                    WeightedObject<Location> weightedObject = locationsWtd.remove(0);
-                    nextHighestMsgRate = weightedObject.getWeight();
-                    Location nextHighestLocation = weightedObject.getObject();
-                    optimalContainerInNextHighest = findOptimal(model.getAvailableContainersFor(item, nextHighestLocation));
-                    if (optimalContainerInNextHighest != null) {
-                        break;
-                    }
-                }
-                if (optimalContainerInNextHighest == null) {
-                    nextHighestMsgRate = current;
-                }
-                
-                if (parameters.isTriggered(highestMsgRate, total, nextHighestMsgRate, current)) {
-                    LOG.info("POLICY "+name+" detected "+itemName+" should be in location "+highestLocation+" on "+optimalContainerInHighest+" ("+highestMsgRate+" of "+total+" msgs/sec), migrating");
-                    try {
-                        if (activeContainer.equals(optimalContainerInHighest)) {
-                            //shouldn't happen
-                            LOG.warn("POLICY "+name+" detected "+itemName+" should move to "+optimalContainerInHighest+" ("+highestMsgRate+" of "+total+" msgs/sec) but it is already there with "+current+" msgs/sec");
-                        } else {
-                            item.move(optimalContainerInHighest);
-                            model.onItemMoved(item, optimalContainerInHighest);
-                        }
-                    } catch (Exception e) {
-                        LOG.warn("POLICY "+name+" detected "+itemName+" should be on "+optimalContainerInHighest+", but can't move it: "+e, e);
-                    }
-                } else {
-                    if (LOG.isTraceEnabled()) LOG.trace("POLICY "+name+" detected "+itemName+" need not move to "+optimalContainerInHighest+" ("+highestMsgRate+" of "+total+" msgs/sec not much better than "+current+" at "+activeContainer+")");
-                }
-            }
-        } catch (Exception e) {
-            LOG.warn("Error in policy "+name+" (ignoring): "+e, e);
-        }
-    }
-
-    private ContainerType findOptimal(Collection<ContainerType> contenders) {
-        /*
-         * TODO should choose the least loaded mediator. Currently chooses first available, and relies 
-         * on a load-balancer to move it again; would be good if these could share decision code so move 
-         * it to the right place immediately. e.g.
-         *   policyUtil.findLeastLoadedMediator(nodesInLocation);
-         */
-        return (contenders.isEmpty() ? null : Iterables.get(contenders, 0));
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/followthesun/WeightedObject.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/followthesun/WeightedObject.java b/policy/src/main/java/brooklyn/policy/followthesun/WeightedObject.java
deleted file mode 100644
index b1d506d..0000000
--- a/policy/src/main/java/brooklyn/policy/followthesun/WeightedObject.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-public class WeightedObject<T> implements Comparable<WeightedObject<T>>{
-    
-    final T object;
-    final double weight;
-    
-    public WeightedObject(T obj, double weight) {
-        this.object = obj;
-        this.weight = weight;
-    }
-    
-    public T getObject() {
-        return object;
-    }
-    
-    public double getWeight() {
-        return weight;
-    }
-
-    /**
-     * Note that equals and compareTo are not consistent: x.compareTo(y)==0 iff x.equals(y) is 
-     * highly recommended in Java, but is not required. This can make TreeSet etc behave poorly...
-     */
-    public int compareTo(WeightedObject<T> o) {
-        double diff = o.getWeight() - weight;
-        if (diff>0.0000000000000001) return -1;
-        if (diff<-0.0000000000000001) return 1;
-        return 0;
-    }
-
-    @Override
-    /** true irrespective of weight */
-    public boolean equals(Object obj) {
-        if (!(obj instanceof WeightedObject<?>)) return false;
-        if (getObject()==null) {
-            return ((WeightedObject<?>)obj).getObject() == null;
-        } else {
-            return getObject().equals( ((WeightedObject<?>)obj).getObject() );
-        }
-    }
-    
-    @Override
-    public int hashCode() {
-        if (getObject()==null) return 234519078;
-        return getObject().hashCode();
-    }
-    
-    @Override
-    public String toString() {
-        return ""+getObject()+"["+getWeight()+"]";
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/ha/AbstractFailureDetector.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/ha/AbstractFailureDetector.java b/policy/src/main/java/brooklyn/policy/ha/AbstractFailureDetector.java
deleted file mode 100644
index 9d8c58f..0000000
--- a/policy/src/main/java/brooklyn/policy/ha/AbstractFailureDetector.java
+++ /dev/null
@@ -1,361 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import static brooklyn.util.time.Time.makeTimeStringRounded;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.management.Task;
-import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-import org.apache.brooklyn.core.util.task.BasicTask;
-import org.apache.brooklyn.core.util.task.ScheduledTask;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.BrooklynTaskTags;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.entity.basic.EntityInternal;
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.exceptions.Exceptions;
-import brooklyn.util.time.Duration;
-import brooklyn.util.time.Time;
-
-import com.google.common.reflect.TypeToken;
-
-public abstract class AbstractFailureDetector extends AbstractPolicy {
-
-    // TODO Remove duplication from ServiceFailureDetector, particularly for the stabilisation delays.
-
-    private static final Logger LOG = LoggerFactory.getLogger(AbstractFailureDetector.class);
-
-    private static final long MIN_PERIOD_BETWEEN_EXECS_MILLIS = 100;
-
-    public static final ConfigKey<Duration> POLL_PERIOD = ConfigKeys.newDurationConfigKey(
-            "failureDetector.pollPeriod", "", Duration.ONE_SECOND);
-
-    @SetFromFlag("failedStabilizationDelay")
-    public static final ConfigKey<Duration> FAILED_STABILIZATION_DELAY = ConfigKeys.newDurationConfigKey(
-            "failureDetector.serviceFailedStabilizationDelay",
-            "Time period for which the health check consistently fails "
-                    + "(e.g. doesn't report failed-ok-faled) before concluding failure.",
-            Duration.ZERO);
-
-    @SetFromFlag("recoveredStabilizationDelay")
-    public static final ConfigKey<Duration> RECOVERED_STABILIZATION_DELAY = ConfigKeys.newDurationConfigKey(
-            "failureDetector.serviceRecoveredStabilizationDelay",
-            "Time period for which the health check succeeds continiually " +
-                    "(e.g. doesn't report ok-failed-ok) before concluding recovered",
-            Duration.ZERO);
-
-    @SuppressWarnings("serial")
-    public static final ConfigKey<Sensor<FailureDescriptor>> SENSOR_FAILED = ConfigKeys.newConfigKey(new TypeToken<Sensor<FailureDescriptor>>() {},
-            "failureDetector.sensor.fail", "A sensor which will indicate failure when set", HASensors.ENTITY_FAILED);
-
-    @SuppressWarnings("serial")
-    public static final ConfigKey<Sensor<FailureDescriptor>> SENSOR_RECOVERED = ConfigKeys.newConfigKey(new TypeToken<Sensor<FailureDescriptor>>() {},
-            "failureDetector.sensor.recover", "A sensor which will indicate recovery from failure when set", HASensors.ENTITY_RECOVERED);
-
-    public interface CalculatedStatus {
-        boolean isHealthy();
-        String getDescription();
-    }
-
-    private final class PublishJob implements Runnable {
-        @Override public void run() {
-            try {
-                executorTime = System.currentTimeMillis();
-                executorQueued.set(false);
-
-                publishNow();
-
-            } catch (Exception e) {
-                if (isRunning()) {
-                    LOG.error("Problem resizing: "+e, e);
-                } else {
-                    if (LOG.isDebugEnabled()) LOG.debug("Problem resizing, but no longer running: "+e, e);
-                }
-            } catch (Throwable t) {
-                LOG.error("Problem in service-failure-detector: "+t, t);
-                throw Exceptions.propagate(t);
-            }
-        }
-    }
-
-    private final class HealthPoller implements Runnable {
-        @Override
-        public void run() {
-            checkHealth();
-        }
-    }
-
-    private final class HealthPollingTaskFactory implements Callable<Task<?>> {
-        @Override
-        public Task<?> call() {
-            BasicTask<Void> task = new BasicTask<Void>(new HealthPoller());
-            BrooklynTaskTags.setTransient(task);
-            return task;
-        }
-    }
-
-    protected static class BasicCalculatedStatus implements CalculatedStatus {
-        private boolean healthy;
-        private String description;
-
-        public BasicCalculatedStatus(boolean healthy, String description) {
-            this.healthy = healthy;
-            this.description = description;
-        }
-
-        @Override
-        public boolean isHealthy() {
-            return healthy;
-        }
-
-        @Override
-        public String getDescription() {
-            return description;
-        }
-    }
-
-    public enum LastPublished {
-        NONE,
-        FAILED,
-        RECOVERED;
-    }
-
-    protected final AtomicReference<Long> stateLastGood = new AtomicReference<Long>();
-    protected final AtomicReference<Long> stateLastFail = new AtomicReference<Long>();
-
-    protected Long currentFailureStartTime = null;
-    protected Long currentRecoveryStartTime = null;
-
-    protected LastPublished lastPublished = LastPublished.NONE;
-
-    private final AtomicBoolean executorQueued = new AtomicBoolean(false);
-    private volatile long executorTime = 0;
-
-    private Callable<Task<?>> pollingTaskFactory = new HealthPollingTaskFactory();
-
-    private Task<?> scheduledTask;
-
-    protected abstract CalculatedStatus calculateStatus();
-
-    @Override
-    public void setEntity(EntityLocal entity) {
-        super.setEntity(entity);
-
-        if (isRunning()) {
-            doStartPolling();
-        }
-    }
-
-    @Override
-    public void suspend() {
-        scheduledTask.cancel(true);
-        super.suspend();
-    }
-
-    @Override
-    public void resume() {
-        currentFailureStartTime = null;
-        currentRecoveryStartTime = null;
-        lastPublished = LastPublished.NONE;
-        executorQueued.set(false);
-        executorTime = 0;
-
-        super.resume();
-        doStartPolling();
-    }
-
-    @SuppressWarnings("unchecked")
-    protected void doStartPolling() {
-        if (scheduledTask == null || scheduledTask.isDone()) {
-            ScheduledTask task = new ScheduledTask(MutableMap.of("period", getPollPeriod(), "displayName", getTaskName()), pollingTaskFactory);
-            scheduledTask = ((EntityInternal)entity).getExecutionContext().submit(task);
-        }
-    }
-
-    private String getTaskName() {
-        return getDisplayName();
-    }
-
-    protected Duration getPollPeriod() {
-        return getConfig(POLL_PERIOD);
-    }
-
-    protected Duration getFailedStabilizationDelay() {
-        return getConfig(FAILED_STABILIZATION_DELAY);
-    }
-
-    protected Duration getRecoveredStabilizationDelay() {
-        return getConfig(RECOVERED_STABILIZATION_DELAY);
-    }
-
-    protected Sensor<FailureDescriptor> getSensorFailed() {
-        return getConfig(SENSOR_FAILED);
-    }
-
-    protected Sensor<FailureDescriptor> getSensorRecovered() {
-        return getConfig(SENSOR_RECOVERED);
-    }
-
-    private synchronized void checkHealth() {
-        CalculatedStatus status = calculateStatus();
-        boolean healthy = status.isHealthy();
-        long now = System.currentTimeMillis();
-
-        if (healthy) {
-            stateLastGood.set(now);
-            if (lastPublished == LastPublished.FAILED) {
-                if (currentRecoveryStartTime == null) {
-                    LOG.info("{} check for {}, now recovering: {}", new Object[] {this, entity, getDescription(status)});
-                    currentRecoveryStartTime = now;
-                    schedulePublish();
-                } else {
-                    if (LOG.isTraceEnabled()) LOG.trace("{} check for {}, continuing recovering: {}", new Object[] {this, entity, getDescription(status)});
-                }
-            } else {
-                if (currentFailureStartTime != null) {
-                    LOG.info("{} check for {}, now healthy: {}", new Object[] {this, entity, getDescription(status)});
-                    currentFailureStartTime = null;
-                } else {
-                    if (LOG.isTraceEnabled()) LOG.trace("{} check for {}, still healthy: {}", new Object[] {this, entity, getDescription(status)});
-                }
-            }
-        } else {
-            stateLastFail.set(now);
-            if (lastPublished != LastPublished.FAILED) {
-                if (currentFailureStartTime == null) {
-                    LOG.info("{} check for {}, now failing: {}", new Object[] {this, entity, getDescription(status)});
-                    currentFailureStartTime = now;
-                    schedulePublish();
-                } else {
-                    if (LOG.isTraceEnabled()) LOG.trace("{} check for {}, continuing failing: {}", new Object[] {this, entity, getDescription(status)});
-                }
-            } else {
-                if (currentRecoveryStartTime != null) {
-                    LOG.info("{} check for {}, now failing: {}", new Object[] {this, entity, getDescription(status)});
-                    currentRecoveryStartTime = null;
-                } else {
-                    if (LOG.isTraceEnabled()) LOG.trace("{} check for {}, still failed: {}", new Object[] {this, entity, getDescription(status)});
-                }
-            }
-        }
-    }
-
-    protected void schedulePublish() {
-        schedulePublish(0);
-    }
-
-    @SuppressWarnings("unchecked")
-    protected void schedulePublish(long delay) {
-        if (isRunning() && executorQueued.compareAndSet(false, true)) {
-            long now = System.currentTimeMillis();
-            delay = Math.max(0, Math.max(delay, (executorTime + MIN_PERIOD_BETWEEN_EXECS_MILLIS) - now));
-            if (LOG.isTraceEnabled()) LOG.trace("{} scheduling publish in {}ms", this, delay);
-
-            Runnable job = new PublishJob();
-
-            ScheduledTask task = new ScheduledTask(MutableMap.of("delay", Duration.of(delay, TimeUnit.MILLISECONDS)), new BasicTask<Void>(job));
-            ((EntityInternal)entity).getExecutionContext().submit(task);
-        }
-    }
-
-    private synchronized void publishNow() {
-        if (!isRunning()) return;
-
-        CalculatedStatus calculatedStatus = calculateStatus();
-        boolean healthy = calculatedStatus.isHealthy();
-
-        Long lastUpTime = stateLastGood.get();
-        Long lastDownTime = stateLastFail.get();
-        long serviceFailedStabilizationDelay = getFailedStabilizationDelay().toMilliseconds();
-        long serviceRecoveredStabilizationDelay = getRecoveredStabilizationDelay().toMilliseconds();
-        long now = System.currentTimeMillis();
-
-        if (healthy) {
-            if (lastPublished == LastPublished.FAILED) {
-                // only publish if consistently up for serviceRecoveredStabilizationDelay
-                long currentRecoveryPeriod = getTimeDiff(now, currentRecoveryStartTime);
-                long sinceLastDownPeriod = getTimeDiff(now, lastDownTime);
-                if (currentRecoveryPeriod > serviceRecoveredStabilizationDelay && sinceLastDownPeriod > serviceRecoveredStabilizationDelay) {
-                    String description = getDescription(calculatedStatus);
-                    LOG.warn("{} check for {}, publishing recovered: {}", new Object[] {this, entity, description});
-                    entity.emit(getSensorRecovered(), new HASensors.FailureDescriptor(entity, description));
-                    lastPublished = LastPublished.RECOVERED;
-                    currentFailureStartTime = null;
-                } else {
-                    long nextAttemptTime = Math.max(serviceRecoveredStabilizationDelay - currentRecoveryPeriod, serviceRecoveredStabilizationDelay - sinceLastDownPeriod);
-                    schedulePublish(nextAttemptTime);
-                }
-            }
-        } else {
-            if (lastPublished != LastPublished.FAILED) {
-                // only publish if consistently down for serviceFailedStabilizationDelay
-                long currentFailurePeriod = getTimeDiff(now, currentFailureStartTime);
-                long sinceLastUpPeriod = getTimeDiff(now, lastUpTime);
-                if (currentFailurePeriod > serviceFailedStabilizationDelay && sinceLastUpPeriod > serviceFailedStabilizationDelay) {
-                    String description = getDescription(calculatedStatus);
-                    LOG.warn("{} connectivity-check for {}, publishing failed: {}", new Object[] {this, entity, description});
-                    entity.emit(getSensorFailed(), new HASensors.FailureDescriptor(entity, description));
-                    lastPublished = LastPublished.FAILED;
-                    currentRecoveryStartTime = null;
-                } else {
-                    long nextAttemptTime = Math.max(serviceFailedStabilizationDelay - currentFailurePeriod, serviceFailedStabilizationDelay - sinceLastUpPeriod);
-                    schedulePublish(nextAttemptTime);
-                }
-            }
-        }
-    }
-
-    protected String getDescription(CalculatedStatus status) {
-        Long lastUpTime = stateLastGood.get();
-        Long lastDownTime = stateLastGood.get();
-        Duration serviceFailedStabilizationDelay = getFailedStabilizationDelay();
-        Duration serviceRecoveredStabilizationDelay = getRecoveredStabilizationDelay();
-
-        return String.format("%s; healthy=%s; timeNow=%s; lastUp=%s; lastDown=%s; lastPublished=%s; "+
-                    "currentFailurePeriod=%s; currentRecoveryPeriod=%s",
-                status.getDescription(),
-                status.isHealthy(),
-                Time.makeDateString(System.currentTimeMillis()),
-                (lastUpTime != null ? Time.makeDateString(lastUpTime) : "<never>"),
-                (lastDownTime != null ? Time.makeDateString(lastDownTime) : "<never>"),
-                lastPublished,
-                (currentFailureStartTime != null ? getTimeStringSince(currentFailureStartTime) : "<none>") + " (stabilization "+makeTimeStringRounded(serviceFailedStabilizationDelay) + ")",
-                (currentRecoveryStartTime != null ? getTimeStringSince(currentRecoveryStartTime) : "<none>") + " (stabilization "+makeTimeStringRounded(serviceRecoveredStabilizationDelay) + ")");
-    }
-
-    private long getTimeDiff(Long recent, Long previous) {
-        return (previous == null) ? recent : (recent - previous);
-    }
-
-    private String getTimeStringSince(Long time) {
-        return time == null ? null : Time.makeTimeStringRounded(System.currentTimeMillis() - time);
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/ha/ConditionalSuspendPolicy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/ha/ConditionalSuspendPolicy.java b/policy/src/main/java/brooklyn/policy/ha/ConditionalSuspendPolicy.java
deleted file mode 100644
index b347949..0000000
--- a/policy/src/main/java/brooklyn/policy/ha/ConditionalSuspendPolicy.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.api.policy.Policy;
-import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.util.javalang.JavaClassNames;
-
-import com.google.common.base.Preconditions;
-
-public class ConditionalSuspendPolicy extends AbstractPolicy {
-    private static final Logger LOG = LoggerFactory.getLogger(ConditionalSuspendPolicy.class);
-
-    @SetFromFlag("suppressSensor")
-    @SuppressWarnings({ "rawtypes", "unchecked" })
-    public static final ConfigKey<Sensor<?>> SUSPEND_SENSOR = (ConfigKey) ConfigKeys.newConfigKey(Sensor.class,
-            "suppressSensor", "Sensor which will suppress the target policy", HASensors.CONNECTION_FAILED); 
-
-    @SetFromFlag("resetSensor")
-    @SuppressWarnings({ "rawtypes", "unchecked" })
-    public static final ConfigKey<Sensor<?>> RESUME_SENSOR = (ConfigKey) ConfigKeys.newConfigKey(Sensor.class,
-            "resetSensor", "Resume target policy when this sensor is observed", HASensors.CONNECTION_RECOVERED);
-
-    @SetFromFlag("target")
-    public static final ConfigKey<Object> SUSPEND_TARGET = ConfigKeys.newConfigKey(Object.class,
-            "target", "The target policy to suspend. Either direct reference or the value of the suspendTarget config on a policy from the same entity.");
-
-    @Override
-    public void setEntity(EntityLocal entity) {
-        super.setEntity(entity);
-        Object target = config().get(SUSPEND_TARGET);
-        Preconditions.checkNotNull(target, "Suspend target required");
-        Preconditions.checkNotNull(getTargetPolicy(), "Can't find target policy set in " + SUSPEND_TARGET.getName() + ": " + target);
-        subscribe();
-        uniqueTag = JavaClassNames.simpleClassName(getClass())+":"+getConfig(SUSPEND_SENSOR).getName()+":"+getConfig(RESUME_SENSOR).getName();
-    }
-
-    private void subscribe() {
-        subscribe(entity, getConfig(SUSPEND_SENSOR), new SensorEventListener<Object>() {
-            @Override public void onEvent(final SensorEvent<Object> event) {
-                if (isRunning()) {
-                    Policy target = getTargetPolicy();
-                    target.suspend();
-                    LOG.debug("Suspended policy " + target + ", triggered by " + event.getSensor() + " = " + event.getValue());
-                }
-            }
-
-        });
-        subscribe(entity, getConfig(RESUME_SENSOR), new SensorEventListener<Object>() {
-            @Override public void onEvent(final SensorEvent<Object> event) {
-                if (isRunning()) {
-                    Policy target = getTargetPolicy();
-                    target.resume();
-                    LOG.debug("Resumed policy " + target + ", triggered by " + event.getSensor() + " = " + event.getValue());
-                }
-            }
-        });
-    }
-
-    private Policy getTargetPolicy() {
-        Object target = config().get(SUSPEND_TARGET);
-        if (target instanceof Policy) {
-            return (Policy)target;
-        } else if (target instanceof String) {
-            for (Policy policy : entity.getPolicies()) {
-                // No way to set config values for keys NOT declared in the policy,
-                // so must use displayName as a generally available config value.
-                if (target.equals(policy.getDisplayName()) || target.equals(policy.getClass().getName())) {
-                    return policy;
-                }
-            }
-        } else {
-            throw new IllegalStateException("Unexpected type " + target.getClass() + " for target " + target);
-        }
-        return null;
-    }
-}


[02/24] incubator-brooklyn git commit: add the creation of default database and user with generated password.

Posted by he...@apache.org.
add the creation of default database and user with generated password.

Also publishes these credentials as sensors


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

Branch: refs/heads/master
Commit: 9458e15167555f5fce4f7cc0ba66056f92d0cf78
Parents: 5ef0b61
Author: Robert Moss <ro...@gmail.com>
Authored: Mon Aug 17 13:45:42 2015 +0100
Committer: Robert Moss <ro...@gmail.com>
Committed: Mon Aug 17 13:45:42 2015 +0100

----------------------------------------------------------------------
 .../database/postgresql/PostgreSqlNode.java     | 17 ++++++++++-
 .../postgresql/PostgreSqlSshDriver.java         | 31 +++++++++++++++++++-
 2 files changed, 46 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/9458e151/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
----------------------------------------------------------------------
diff --git a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
index 793debf..e8496f3 100644
--- a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
+++ b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
@@ -22,6 +22,7 @@ import org.apache.brooklyn.api.catalog.Catalog;
 import org.apache.brooklyn.api.entity.Effector;
 import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
 import org.apache.brooklyn.api.entity.trait.HasShortName;
+import org.apache.brooklyn.location.basic.PortRanges;
 
 import brooklyn.config.ConfigKey;
 import brooklyn.entity.basic.ConfigKeys;
@@ -30,8 +31,8 @@ import brooklyn.entity.database.DatabaseNode;
 import brooklyn.entity.database.DatastoreMixins;
 import brooklyn.entity.database.DatastoreMixins.DatastoreCommon;
 import brooklyn.entity.effector.Effectors;
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
 import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
-import org.apache.brooklyn.location.basic.PortRanges;
 import brooklyn.util.flags.SetFromFlag;
 
 /**
@@ -80,6 +81,20 @@ public interface PostgreSqlNode extends SoftwareProcess, HasShortName, Datastore
     @SetFromFlag("pollPeriod")
     ConfigKey<Long> POLL_PERIOD = ConfigKeys.newLongConfigKey(
             "postgresql.sensorpoll", "Poll period (in milliseconds)", 1000L);
+    
+    @SetFromFlag("username")
+    BasicAttributeSensorAndConfigKey<String> USERNAME = new BasicAttributeSensorAndConfigKey<>(
+            String.class, "postgresql.username", "Username of the database user",
+            "postgresuser");
+    
+    @SetFromFlag("password")
+    BasicAttributeSensorAndConfigKey<String> PASSWORD = new BasicAttributeSensorAndConfigKey<>(
+            String.class, "postgresql.password",
+            "Password for the database user, auto-generated if not set");
+
+    @SetFromFlag("database")
+    BasicAttributeSensorAndConfigKey<String> DATABASE = new BasicAttributeSensorAndConfigKey<>(
+            String.class, "postgresql.database", "Database to be used", "db");
 
     Effector<String> EXECUTE_SCRIPT = Effectors.effector(DatastoreMixins.EXECUTE_SCRIPT)
             .description("Executes the given script contents using psql")

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/9458e151/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
----------------------------------------------------------------------
diff --git a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
index a23f2bc..18dc9a4 100644
--- a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
+++ b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
@@ -47,6 +47,7 @@ import brooklyn.entity.basic.SoftwareProcess;
 import brooklyn.entity.database.DatastoreMixins;
 import brooklyn.entity.software.SshEffectorTasks;
 
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
 import org.apache.brooklyn.api.location.OsDetails;
 import org.apache.brooklyn.location.basic.SshMachineLocation;
 
@@ -294,7 +295,24 @@ public class PostgreSqlSshDriver extends AbstractSoftwareProcessSshDriver implem
 
         // Wait for commands to complete before running the creation script
         DynamicTasks.waitForLast();
-
+        String createUserCommand = String.format(
+                "\"CREATE USER %s WITH PASSWORD '%s'; \"",
+                entity.getConfig(PostgreSqlNode.USERNAME), getUserPassword()
+        );
+        String createDatabaseCommand = String.format(
+                "\"CREATE DATABASE %s OWNER %s\"",
+                entity.getConfig(PostgreSqlNode.DATABASE),
+                entity.getConfig(PostgreSqlNode.USERNAME));
+        newScript("initializing user and database")
+        .body.append(
+                "cd " + getInstallDir(),
+                callPgctl("start", true),
+                sudoAsUser("postgres", getInstallDir() + "/bin/psql -p " + entity.getAttribute(PostgreSqlNode.POSTGRESQL_PORT) + 
+                        " --command="+ createUserCommand),
+                sudoAsUser("postgres", getInstallDir() + "/bin/psql -p " + entity.getAttribute(PostgreSqlNode.POSTGRESQL_PORT) + 
+                                " --command="+ createDatabaseCommand),
+                callPgctl("stop", true))
+                .failOnNonZeroResultCode().execute();
         // Capture log file contents if there is an error configuring the database
         try {
             executeDatabaseCreationScript();
@@ -307,6 +325,17 @@ public class PostgreSqlSshDriver extends AbstractSoftwareProcessSshDriver implem
         // on port 5432?" error then the port is probably closed. Check that the firewall allows external TCP/IP
         // connections (netstat -nap). You can open a port with lokkit or by configuring the iptables.
     }
+    
+    protected String getUserPassword() {
+        String password = entity.getConfig(PostgreSqlNode.PASSWORD);
+        if (Strings.isEmpty(password)) {
+            log.debug(entity + " has no password specified for " + PostgreSqlNode.PASSWORD + "; using a random string");
+            password = brooklyn.util.text.Strings.makeRandomId(8);
+            entity.setAttribute(PostgreSqlNode.PASSWORD, password);
+            entity.config().set(PostgreSqlNode.PASSWORD, password);
+        }
+        return password;
+    }
 
     protected void executeDatabaseCreationScript() {
         if (copyDatabaseCreationScript()) {


[15/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunParameters.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunParameters.java b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunParameters.java
new file mode 100644
index 0000000..c8742e4
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunParameters.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.api.location.Location;
+
+public class FollowTheSunParameters {
+
+    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunParameters.class);
+
+    private FollowTheSunParameters() {}
+
+    /** trigger for moving segment X from geo A to geo B:
+     * where x is total number of requests submitted in X across the CDM network,
+     * and x_A is number of reqs from geo A, with A the most prolific geography
+     * (arbitrarily chosen in case of ties so recommended to choose at least a small percent_majority or delta_above_percent_majority, in addition to this field);
+     * this parameter T defines a number such that x_A > T*x in order for X to be migrated to A
+     * (but see also DELTA_ABOVE_PERCENT_TOTAL, below) */
+    public double triggerPercentTotal = 0.3;
+    /** fields as above, and T as above,
+     * this parameter T' defines a number such that x_A > T*x + T' in order for X to be migrated to A */
+    public double triggerDeltaAbovePercentTotal = 0;
+    /** fields as above,
+     * this parameter T defines a number such that x_A > T in order for X to be migrated to A */
+    public double triggerAbsoluteTotal = 2;
+
+    /** fields as above, with X_B the number from a different geography B,
+     * where A and B are the two most prolific requesters of X, and X_A >= X_B;
+     * this parameter T defines a number such that x_A-x_B > T*x in order for X to be migrated to A */
+    public double triggerPercentMajority = 0.2;
+    /** as corresponding majority and total fields, with x_A-x_B on the LHS of inequality */
+    public double triggerDeltaAbovePercentMajority = 1;
+    /** as corresponding majority and total fields, with x_A-x_B on the LHS of inequality */
+    public double triggerAbsoluteMajority = 4;
+    
+    /** a list of excluded locations */
+    public Set<Location> excludedLocations = new LinkedHashSet<Location>();
+
+    public static FollowTheSunParameters newDefault() {
+        return new FollowTheSunParameters();
+    }
+
+    private static double parseDouble(String text, double defaultValue) {
+        try {
+            double d = Double.parseDouble(text);
+            if (!Double.isNaN(d)) return d;
+        } catch (Exception e) {
+            LOG.warn("Illegal double value '"+text+"', using default "+defaultValue+": "+e, e);
+        }
+        return defaultValue;
+    }
+
+    private static String[] parseCommaSeparatedList(String csv) {
+        if (csv==null || csv.trim().length()==0) return new String[0];
+        return csv.split(",");
+    }
+
+    public boolean isTriggered(double highest, double total, double nextHighest, double current) {
+        if (highest <= current) return false;
+        if (highest < total*triggerPercentTotal + triggerDeltaAbovePercentTotal) return false;
+        if (highest < triggerAbsoluteTotal) return false;
+        //TODO more params about nextHighest vs current
+        if (highest-current < total*triggerPercentMajority + triggerDeltaAbovePercentMajority) return false;
+        if (highest-current < triggerAbsoluteMajority) return false;
+        return true;
+    }
+    
+    public String toString() {
+        return "Inter-geography policy params: percentTotal="+triggerPercentTotal+"; deltaAbovePercentTotal="+triggerDeltaAbovePercentTotal+
+                "; absoluteTotal="+triggerAbsoluteTotal+"; percentMajority="+triggerPercentMajority+
+                "; deltaAbovePercentMajority="+triggerDeltaAbovePercentMajority+"; absoluteMajority="+triggerAbsoluteMajority;
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicy.java b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicy.java
new file mode 100644
index 0000000..a02285e
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicy.java
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import static brooklyn.util.JavaGroovyEquivalents.elvis;
+import static brooklyn.util.JavaGroovyEquivalents.groovyTruth;
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.event.AttributeSensor;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.Attributes;
+
+import org.apache.brooklyn.policy.followthesun.FollowTheSunPool.ContainerItemPair;
+import org.apache.brooklyn.policy.loadbalancing.Movable;
+
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+    // removed from catalog because it cannot currently be configured via catalog mechanisms - 
+    // PolicySpec.create fails due to no no-arg constructor
+    // TODO make model and parameters things which can be initialized from config then reinstate in catalog
+//@Catalog(name="Follow the Sun", description="Policy for moving \"work\" around to follow the demand; "
+//        + "the work can be any \"Movable\" entity")
+public class FollowTheSunPolicy extends AbstractPolicy {
+
+    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunPolicy.class);
+
+    public static final String NAME = "Follow the Sun (Inter-Geography Latency Optimization)";
+
+    @SetFromFlag(defaultVal="100")
+    private long minPeriodBetweenExecs;
+    
+    @SetFromFlag
+    private Function<Entity, Location> locationFinder;
+    
+    private final AttributeSensor<Map<? extends Movable, Double>> itemUsageMetric;
+    private final FollowTheSunModel<Entity, Movable> model;
+    private final FollowTheSunStrategy<Entity, Movable> strategy;
+    private final FollowTheSunParameters parameters;
+    
+    private FollowTheSunPool poolEntity;
+    
+    private volatile ScheduledExecutorService executor;
+    private final AtomicBoolean executorQueued = new AtomicBoolean(false);
+    private volatile long executorTime = 0;
+    private boolean loggedConstraintsIgnored = false;
+    
+    private final Function<Entity, Location> defaultLocationFinder = new Function<Entity, Location>() {
+        public Location apply(Entity e) {
+            Collection<Location> locs = e.getLocations();
+            if (locs.isEmpty()) return null;
+            Location contender = Iterables.get(locs, 0);
+            while (contender.getParent() != null && !(contender instanceof MachineProvisioningLocation)) {
+                contender = contender.getParent();
+            }
+            return contender;
+        }
+    };
+    
+    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
+        @Override
+        public void onEvent(SensorEvent<Object> event) {
+            if (LOG.isTraceEnabled()) LOG.trace("{} received event {}", FollowTheSunPolicy.this, event);
+            Entity source = event.getSource();
+            Object value = event.getValue();
+            Sensor<?> sensor = event.getSensor();
+            
+            if (sensor.equals(itemUsageMetric)) {
+                onItemMetricUpdated((Movable)source, (Map<? extends Movable, Double>) value, true);
+            } else if (sensor.equals(Attributes.LOCATION_CHANGED)) {
+                onContainerLocationUpdated(source, true);
+            } else if (sensor.equals(FollowTheSunPool.CONTAINER_ADDED)) {
+                onContainerAdded((Entity) value, true);
+            } else if (sensor.equals(FollowTheSunPool.CONTAINER_REMOVED)) {
+                onContainerRemoved((Entity) value, true);
+            } else if (sensor.equals(FollowTheSunPool.ITEM_ADDED)) {
+                onItemAdded((Movable) value, true);
+            } else if (sensor.equals(FollowTheSunPool.ITEM_REMOVED)) {
+                onItemRemoved((Movable) value, true);
+            } else if (sensor.equals(FollowTheSunPool.ITEM_MOVED)) {
+                ContainerItemPair pair = (ContainerItemPair) value;
+                onItemMoved((Movable)pair.item, pair.container, true);
+            }
+        }
+    };
+    
+    // FIXME parameters: use a more groovy way of doing it, that's consistent with other policies/entities?
+    public FollowTheSunPolicy(AttributeSensor itemUsageMetric, 
+            FollowTheSunModel<Entity, Movable> model, FollowTheSunParameters parameters) {
+        this(MutableMap.of(), itemUsageMetric, model, parameters);
+    }
+    
+    public FollowTheSunPolicy(Map props, AttributeSensor itemUsageMetric, 
+            FollowTheSunModel<Entity, Movable> model, FollowTheSunParameters parameters) {
+        super(props);
+        this.itemUsageMetric = itemUsageMetric;
+        this.model = model;
+        this.parameters = parameters;
+        this.strategy = new FollowTheSunStrategy<Entity, Movable>(model, parameters); // TODO: extract interface, inject impl
+        this.locationFinder = elvis(locationFinder, defaultLocationFinder);
+        
+        // TODO Should re-use the execution manager's thread pool, somehow
+        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
+    }
+    
+    @Override
+    public void setEntity(EntityLocal entity) {
+        checkArgument(entity instanceof FollowTheSunPool, "Provided entity must be a FollowTheSunPool");
+        super.setEntity(entity);
+        this.poolEntity = (FollowTheSunPool) entity;
+        
+        // Detect when containers are added to or removed from the pool.
+        subscribe(poolEntity, FollowTheSunPool.CONTAINER_ADDED, eventHandler);
+        subscribe(poolEntity, FollowTheSunPool.CONTAINER_REMOVED, eventHandler);
+        subscribe(poolEntity, FollowTheSunPool.ITEM_ADDED, eventHandler);
+        subscribe(poolEntity, FollowTheSunPool.ITEM_REMOVED, eventHandler);
+        subscribe(poolEntity, FollowTheSunPool.ITEM_MOVED, eventHandler);
+        
+        // Take heed of any extant containers.
+        for (Entity container : poolEntity.getContainerGroup().getMembers()) {
+            onContainerAdded(container, false);
+        }
+        for (Entity item : poolEntity.getItemGroup().getMembers()) {
+            onItemAdded((Movable)item, false);
+        }
+
+        scheduleLatencyReductionJig();
+    }
+    
+    @Override
+    public void suspend() {
+        // TODO unsubscribe from everything? And resubscribe on resume?
+        super.suspend();
+        if (executor != null) executor.shutdownNow();
+        executorQueued.set(false);
+    }
+    
+    @Override
+    public void resume() {
+        super.resume();
+        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
+        executorTime = 0;
+        executorQueued.set(false);
+    }
+    
+    private ThreadFactory newThreadFactory() {
+        return new ThreadFactoryBuilder()
+                .setNameFormat("brooklyn-followthesunpolicy-%d")
+                .build();
+    }
+
+    private void scheduleLatencyReductionJig() {
+        if (isRunning() && executorQueued.compareAndSet(false, true)) {
+            long now = System.currentTimeMillis();
+            long delay = Math.max(0, (executorTime + minPeriodBetweenExecs) - now);
+            
+            executor.schedule(new Runnable() {
+                public void run() {
+                    try {
+                        executorTime = System.currentTimeMillis();
+                        executorQueued.set(false);
+                        
+                        if (LOG.isTraceEnabled()) LOG.trace("{} executing follow-the-sun migration-strategy", this);
+                        strategy.rebalance();
+                        
+                    } catch (RuntimeException e) {
+                        if (isRunning()) {
+                            LOG.error("Error during latency-reduction-jig", e);
+                        } else {
+                            LOG.debug("Error during latency-reduction-jig, but no longer running", e);
+                        }
+                    }
+                }},
+                delay,
+                TimeUnit.MILLISECONDS);
+        }
+    }
+    
+    private void onContainerAdded(Entity container, boolean rebalanceNow) {
+        subscribe(container, Attributes.LOCATION_CHANGED, eventHandler);
+        Location location = locationFinder.apply(container);
+        
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording addition of container {} in location {}", new Object[] {this, container, location});
+        model.onContainerAdded(container, location);
+        
+        if (rebalanceNow) scheduleLatencyReductionJig();
+    }
+    
+    private void onContainerRemoved(Entity container, boolean rebalanceNow) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording removal of container {}", this, container);
+        model.onContainerRemoved(container);
+        if (rebalanceNow) scheduleLatencyReductionJig();
+    }
+    
+    private void onItemAdded(Movable item, boolean rebalanceNow) {
+        Entity parentContainer = (Entity) item.getAttribute(Movable.CONTAINER);
+        
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording addition of item {} in container {}", new Object[] {this, item, parentContainer});
+        
+        subscribe(item, itemUsageMetric, eventHandler);
+        
+        // Update the model, including the current metric value (if any).
+        Map<? extends Movable, Double> currentValue = item.getAttribute(itemUsageMetric);
+        boolean immovable = (Boolean)elvis(item.getConfig(Movable.IMMOVABLE), false);
+        model.onItemAdded(item, parentContainer, immovable);
+
+        if (currentValue != null) {
+            model.onItemUsageUpdated(item, currentValue);
+        }
+        
+        if (rebalanceNow) scheduleLatencyReductionJig();
+    }
+    
+    private void onItemRemoved(Movable item, boolean rebalanceNow) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording removal of item {}", this, item);
+        unsubscribe(item);
+        model.onItemRemoved(item);
+        if (rebalanceNow) scheduleLatencyReductionJig();
+    }
+    
+    private void onItemMoved(Movable item, Entity parentContainer, boolean rebalanceNow) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording moving of item {} to {}", new Object[] {this, item, parentContainer});
+        model.onItemMoved(item, parentContainer);
+        if (rebalanceNow) scheduleLatencyReductionJig();
+    }
+    
+    private void onContainerLocationUpdated(Entity container, boolean rebalanceNow) {
+        Location location = locationFinder.apply(container);
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording location for container {}, new value {}", new Object[] {this, container, location});
+        model.onContainerLocationUpdated(container, location);
+        if (rebalanceNow) scheduleLatencyReductionJig();
+    }
+    
+    private void onItemMetricUpdated(Movable item, Map<? extends Movable, Double> newValues, boolean rebalanceNow) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording usage update for item {}, new value {}", new Object[] {this, item, newValues});
+        model.onItemUsageUpdated(item, newValues);
+        if (rebalanceNow) scheduleLatencyReductionJig();
+    }
+    
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + (groovyTruth(name) ? "("+name+")" : "");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPool.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPool.java b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPool.java
new file mode 100644
index 0000000..7dc668e
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPool.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.Serializable;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
+
+import brooklyn.entity.trait.Resizable;
+import brooklyn.event.basic.BasicNotificationSensor;
+
+@ImplementedBy(FollowTheSunPoolImpl.class)
+public interface FollowTheSunPool extends Entity, Resizable {
+
+    // FIXME Remove duplication from BalanceableWorkerPool?
+
+    // FIXME Asymmetry between loadbalancing and followTheSun: ITEM_ADDED and ITEM_REMOVED in loadbalancing
+    // are of type ContainerItemPair, but in followTheSun it is just the `Entity item`.
+    
+    /** Encapsulates an item and a container; emitted by sensors.
+     */
+    public static class ContainerItemPair implements Serializable {
+        private static final long serialVersionUID = 1L;
+        public final Entity container;
+        public final Entity item;
+
+        public ContainerItemPair(Entity container, Entity item) {
+            this.container = container;
+            this.item = checkNotNull(item);
+        }
+
+        @Override
+        public String toString() {
+            return ""+item+" @ "+container;
+        }
+    }
+
+    // Pool constituent notifications.
+    public static BasicNotificationSensor<Entity> CONTAINER_ADDED = new BasicNotificationSensor<Entity>(
+            Entity.class, "followthesun.container.added", "Container added");
+    public static BasicNotificationSensor<Entity> CONTAINER_REMOVED = new BasicNotificationSensor<Entity>(
+            Entity.class, "followthesun.container.removed", "Container removed");
+    public static BasicNotificationSensor<Entity> ITEM_ADDED = new BasicNotificationSensor<Entity>(
+            Entity.class, "followthesun.item.added", "Item added");
+    public static BasicNotificationSensor<Entity> ITEM_REMOVED = new BasicNotificationSensor<Entity>(
+            Entity.class, "followthesun.item.removed", "Item removed");
+    public static BasicNotificationSensor<ContainerItemPair> ITEM_MOVED = new BasicNotificationSensor<ContainerItemPair>(
+            ContainerItemPair.class, "followthesun.item.moved", "Item moved to the given container");
+
+    public void setContents(Group containerGroup, Group itemGroup);
+
+    public Group getContainerGroup();
+
+    public Group getItemGroup();
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPoolImpl.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPoolImpl.java b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPoolImpl.java
new file mode 100644
index 0000000..b9c597e
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPoolImpl.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.AbstractEntity;
+import brooklyn.entity.basic.AbstractGroup;
+import brooklyn.entity.trait.Resizable;
+import brooklyn.entity.trait.Startable;
+import org.apache.brooklyn.policy.loadbalancing.Movable;
+
+public class FollowTheSunPoolImpl extends AbstractEntity implements FollowTheSunPool {
+
+    // FIXME Remove duplication from BalanceableWorkerPool?
+
+    // FIXME Asymmetry between loadbalancing and followTheSun: ITEM_ADDED and ITEM_REMOVED in loadbalancing
+    // are of type ContainerItemPair, but in followTheSun it is just the `Entity item`.
+    
+    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunPool.class);
+
+    private Group containerGroup;
+    private Group itemGroup;
+
+    private final Set<Entity> containers = Collections.synchronizedSet(new HashSet<Entity>());
+    private final Set<Entity> items = Collections.synchronizedSet(new HashSet<Entity>());
+
+    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
+        @Override
+        public void onEvent(SensorEvent<Object> event) {
+            if (LOG.isTraceEnabled()) LOG.trace("{} received event {}", FollowTheSunPoolImpl.this, event);
+            Entity source = event.getSource();
+            Object value = event.getValue();
+            Sensor sensor = event.getSensor();
+
+            if (sensor.equals(AbstractGroup.MEMBER_ADDED)) {
+                if (source.equals(containerGroup)) {
+                    onContainerAdded((Entity) value);
+                } else if (source.equals(itemGroup)) {
+                    onItemAdded((Entity)value);
+                } else {
+                    throw new IllegalStateException("unexpected event source="+source);
+                }
+            } else if (sensor.equals(AbstractGroup.MEMBER_REMOVED)) {
+                if (source.equals(containerGroup)) {
+                    onContainerRemoved((Entity) value);
+                } else if (source.equals(itemGroup)) {
+                    onItemRemoved((Entity) value);
+                } else {
+                    throw new IllegalStateException("unexpected event source="+source);
+                }
+            } else if (sensor.equals(Startable.SERVICE_UP)) {
+                // TODO What if start has failed? Is there a sensor to indicate that?
+                if ((Boolean)value) {
+                    onContainerUp(source);
+                } else {
+                    onContainerDown(source);
+                }
+            } else if (sensor.equals(Movable.CONTAINER)) {
+                onItemMoved(source, (Entity) value);
+            } else {
+                throw new IllegalStateException("Unhandled event type "+sensor+": "+event);
+            }
+        }
+    };
+
+    public FollowTheSunPoolImpl() {
+    }
+
+    @Override
+    public void setContents(Group containerGroup, Group itemGroup) {
+        this.containerGroup = containerGroup;
+        this.itemGroup = itemGroup;
+        subscribe(containerGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
+        subscribe(containerGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
+        subscribe(itemGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
+        subscribe(itemGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
+
+        // Process extant containers and items
+        for (Entity existingContainer : containerGroup.getMembers()) {
+            onContainerAdded(existingContainer);
+        }
+        for (Entity existingItem : itemGroup.getMembers()) {
+            onItemAdded((Entity)existingItem);
+        }
+    }
+
+    @Override
+    public Group getContainerGroup() {
+        return containerGroup;
+    }
+
+    @Override
+    public Group getItemGroup() {
+        return itemGroup;
+    }
+
+    @Override
+    public Integer getCurrentSize() {
+        return containerGroup.getCurrentSize();
+    }
+
+    @Override
+    public Integer resize(Integer desiredSize) {
+        if (containerGroup instanceof Resizable) return ((Resizable) containerGroup).resize(desiredSize);
+
+        throw new UnsupportedOperationException("Container group is not resizable");
+    }
+
+
+    private void onContainerAdded(Entity newContainer) {
+        subscribe(newContainer, Startable.SERVICE_UP, eventHandler);
+        if (!(newContainer instanceof Startable) || Boolean.TRUE.equals(newContainer.getAttribute(Startable.SERVICE_UP))) {
+            onContainerUp(newContainer);
+        }
+    }
+
+    private void onContainerUp(Entity newContainer) {
+        if (containers.add(newContainer)) {
+            emit(CONTAINER_ADDED, newContainer);
+        }
+    }
+
+    private void onContainerDown(Entity oldContainer) {
+        if (containers.remove(oldContainer)) {
+            emit(CONTAINER_REMOVED, oldContainer);
+        }
+    }
+
+    private void onContainerRemoved(Entity oldContainer) {
+        unsubscribe(oldContainer);
+        onContainerDown(oldContainer);
+    }
+
+    private void onItemAdded(Entity item) {
+        if (items.add(item)) {
+            subscribe(item, Movable.CONTAINER, eventHandler);
+            emit(ITEM_ADDED, item);
+        }
+    }
+
+    private void onItemRemoved(Entity item) {
+        if (items.remove(item)) {
+            unsubscribe(item);
+            emit(ITEM_REMOVED, item);
+        }
+    }
+
+    private void onItemMoved(Entity item, Entity container) {
+        emit(ITEM_MOVED, new ContainerItemPair(container, item));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunStrategy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunStrategy.java b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunStrategy.java
new file mode 100644
index 0000000..68d6ae2
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunStrategy.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.brooklyn.policy.loadbalancing.Movable;
+
+import com.google.common.collect.Iterables;
+
+// TODO: extract interface
+public class FollowTheSunStrategy<ContainerType extends Entity, ItemType extends Movable> {
+    
+    // This is a modified version of the InterGeographyLatencyPolicy (aka Follow-The-Sun) policy from Monterey v3.
+    
+    // TODO location constraints
+    
+    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunStrategy.class);
+    
+    private final FollowTheSunParameters parameters;
+    private final FollowTheSunModel<ContainerType,ItemType> model;
+    private final String name;
+    
+    public FollowTheSunStrategy(FollowTheSunModel<ContainerType,ItemType> model, FollowTheSunParameters parameters) {
+        this.model = model;
+        this.parameters = parameters;
+        this.name = model.getName();
+    }
+    
+    public void rebalance() {
+        try {
+            Set<ItemType> items = model.getItems();
+            Map<ItemType, Map<Location, Double>> directSendsToItemByLocation = model.getDirectSendsToItemByLocation();
+            
+            for (ItemType item : items) {
+                String itemName = model.getName(item);
+                Location activeLocation = model.getItemLocation(item);
+                ContainerType activeContainer = model.getItemContainer(item);
+                Map<Location, Double> sendsByLocation = directSendsToItemByLocation.get(item);
+                if (sendsByLocation == null) sendsByLocation = Collections.emptyMap();
+                
+                if (parameters.excludedLocations.contains(activeLocation)) {
+                    if (LOG.isTraceEnabled()) LOG.trace("Ignoring segment {} as it is in {}", itemName, activeLocation);
+                    continue;
+                }
+                if (!model.isItemMoveable(item)) {
+                    if (LOG.isDebugEnabled()) LOG.debug("POLICY {} skipping any migration of {}, it is not moveable", name, itemName);
+                    continue;
+                }
+                if (model.hasActiveMigration(item)) {
+                    LOG.info("POLICY {} skipping any migration of {}, it is involved in an active migration already", name, itemName);
+                    continue;
+                }
+                
+                double total = DefaultFollowTheSunModel.sum(sendsByLocation.values());
+
+                if (LOG.isTraceEnabled()) LOG.trace("POLICY {} detected {} msgs/sec in {}, split up as: {}", new Object[] {name, total, itemName, sendsByLocation});
+                
+                Double current = sendsByLocation.get(activeLocation);
+                if (current == null) current=0d;
+                List<WeightedObject<Location>> locationsWtd = new ArrayList<WeightedObject<Location>>();
+                if (total > 0) {
+                    for (Map.Entry<Location, Double> entry : sendsByLocation.entrySet()) {
+                        Location l = entry.getKey();
+                        Double d = entry.getValue();
+                        if (d > current) locationsWtd.add(new WeightedObject<Location>(l, d));
+                    }
+                }
+                Collections.sort(locationsWtd);
+                Collections.reverse(locationsWtd);
+                
+                double highestMsgRate = -1;
+                Location highestLocation = null;
+                ContainerType optimalContainerInHighest = null;
+                while (!locationsWtd.isEmpty()) {
+                    WeightedObject<Location> weightedObject = locationsWtd.remove(0);
+                    highestMsgRate = weightedObject.getWeight();
+                    highestLocation = weightedObject.getObject();
+                    optimalContainerInHighest = findOptimal(model.getAvailableContainersFor(item, highestLocation));
+                    if (optimalContainerInHighest != null) {
+                        break;
+                    }
+                }
+                if (optimalContainerInHighest == null) {
+                    if (LOG.isDebugEnabled()) LOG.debug("POLICY {} detected {} is already in optimal permitted location ({} of {} msgs/sec)", new Object[] {name, itemName, highestMsgRate, total});
+                    continue;                   
+                }
+                
+                double nextHighestMsgRate = -1;
+                ContainerType optimalContainerInNextHighest = null;
+                while (!locationsWtd.isEmpty()) {
+                    WeightedObject<Location> weightedObject = locationsWtd.remove(0);
+                    nextHighestMsgRate = weightedObject.getWeight();
+                    Location nextHighestLocation = weightedObject.getObject();
+                    optimalContainerInNextHighest = findOptimal(model.getAvailableContainersFor(item, nextHighestLocation));
+                    if (optimalContainerInNextHighest != null) {
+                        break;
+                    }
+                }
+                if (optimalContainerInNextHighest == null) {
+                    nextHighestMsgRate = current;
+                }
+                
+                if (parameters.isTriggered(highestMsgRate, total, nextHighestMsgRate, current)) {
+                    LOG.info("POLICY "+name+" detected "+itemName+" should be in location "+highestLocation+" on "+optimalContainerInHighest+" ("+highestMsgRate+" of "+total+" msgs/sec), migrating");
+                    try {
+                        if (activeContainer.equals(optimalContainerInHighest)) {
+                            //shouldn't happen
+                            LOG.warn("POLICY "+name+" detected "+itemName+" should move to "+optimalContainerInHighest+" ("+highestMsgRate+" of "+total+" msgs/sec) but it is already there with "+current+" msgs/sec");
+                        } else {
+                            item.move(optimalContainerInHighest);
+                            model.onItemMoved(item, optimalContainerInHighest);
+                        }
+                    } catch (Exception e) {
+                        LOG.warn("POLICY "+name+" detected "+itemName+" should be on "+optimalContainerInHighest+", but can't move it: "+e, e);
+                    }
+                } else {
+                    if (LOG.isTraceEnabled()) LOG.trace("POLICY "+name+" detected "+itemName+" need not move to "+optimalContainerInHighest+" ("+highestMsgRate+" of "+total+" msgs/sec not much better than "+current+" at "+activeContainer+")");
+                }
+            }
+        } catch (Exception e) {
+            LOG.warn("Error in policy "+name+" (ignoring): "+e, e);
+        }
+    }
+
+    private ContainerType findOptimal(Collection<ContainerType> contenders) {
+        /*
+         * TODO should choose the least loaded mediator. Currently chooses first available, and relies 
+         * on a load-balancer to move it again; would be good if these could share decision code so move 
+         * it to the right place immediately. e.g.
+         *   policyUtil.findLeastLoadedMediator(nodesInLocation);
+         */
+        return (contenders.isEmpty() ? null : Iterables.get(contenders, 0));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/followthesun/WeightedObject.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/followthesun/WeightedObject.java b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/WeightedObject.java
new file mode 100644
index 0000000..e1caf1f
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/WeightedObject.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+public class WeightedObject<T> implements Comparable<WeightedObject<T>>{
+    
+    final T object;
+    final double weight;
+    
+    public WeightedObject(T obj, double weight) {
+        this.object = obj;
+        this.weight = weight;
+    }
+    
+    public T getObject() {
+        return object;
+    }
+    
+    public double getWeight() {
+        return weight;
+    }
+
+    /**
+     * Note that equals and compareTo are not consistent: x.compareTo(y)==0 iff x.equals(y) is 
+     * highly recommended in Java, but is not required. This can make TreeSet etc behave poorly...
+     */
+    public int compareTo(WeightedObject<T> o) {
+        double diff = o.getWeight() - weight;
+        if (diff>0.0000000000000001) return -1;
+        if (diff<-0.0000000000000001) return 1;
+        return 0;
+    }
+
+    @Override
+    /** true irrespective of weight */
+    public boolean equals(Object obj) {
+        if (!(obj instanceof WeightedObject<?>)) return false;
+        if (getObject()==null) {
+            return ((WeightedObject<?>)obj).getObject() == null;
+        } else {
+            return getObject().equals( ((WeightedObject<?>)obj).getObject() );
+        }
+    }
+    
+    @Override
+    public int hashCode() {
+        if (getObject()==null) return 234519078;
+        return getObject().hashCode();
+    }
+    
+    @Override
+    public String toString() {
+        return ""+getObject()+"["+getWeight()+"]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/ha/AbstractFailureDetector.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/ha/AbstractFailureDetector.java b/policy/src/main/java/org/apache/brooklyn/policy/ha/AbstractFailureDetector.java
new file mode 100644
index 0000000..d7af68a
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/ha/AbstractFailureDetector.java
@@ -0,0 +1,363 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import static brooklyn.util.time.Time.makeTimeStringRounded;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.management.Task;
+import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+import org.apache.brooklyn.core.util.task.BasicTask;
+import org.apache.brooklyn.core.util.task.ScheduledTask;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.BrooklynTaskTags;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.EntityInternal;
+
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+
+import com.google.common.reflect.TypeToken;
+
+public abstract class AbstractFailureDetector extends AbstractPolicy {
+
+    // TODO Remove duplication from ServiceFailureDetector, particularly for the stabilisation delays.
+
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractFailureDetector.class);
+
+    private static final long MIN_PERIOD_BETWEEN_EXECS_MILLIS = 100;
+
+    public static final ConfigKey<Duration> POLL_PERIOD = ConfigKeys.newDurationConfigKey(
+            "failureDetector.pollPeriod", "", Duration.ONE_SECOND);
+
+    @SetFromFlag("failedStabilizationDelay")
+    public static final ConfigKey<Duration> FAILED_STABILIZATION_DELAY = ConfigKeys.newDurationConfigKey(
+            "failureDetector.serviceFailedStabilizationDelay",
+            "Time period for which the health check consistently fails "
+                    + "(e.g. doesn't report failed-ok-faled) before concluding failure.",
+            Duration.ZERO);
+
+    @SetFromFlag("recoveredStabilizationDelay")
+    public static final ConfigKey<Duration> RECOVERED_STABILIZATION_DELAY = ConfigKeys.newDurationConfigKey(
+            "failureDetector.serviceRecoveredStabilizationDelay",
+            "Time period for which the health check succeeds continiually " +
+                    "(e.g. doesn't report ok-failed-ok) before concluding recovered",
+            Duration.ZERO);
+
+    @SuppressWarnings("serial")
+    public static final ConfigKey<Sensor<FailureDescriptor>> SENSOR_FAILED = ConfigKeys.newConfigKey(new TypeToken<Sensor<FailureDescriptor>>() {},
+            "failureDetector.sensor.fail", "A sensor which will indicate failure when set", HASensors.ENTITY_FAILED);
+
+    @SuppressWarnings("serial")
+    public static final ConfigKey<Sensor<FailureDescriptor>> SENSOR_RECOVERED = ConfigKeys.newConfigKey(new TypeToken<Sensor<FailureDescriptor>>() {},
+            "failureDetector.sensor.recover", "A sensor which will indicate recovery from failure when set", HASensors.ENTITY_RECOVERED);
+
+    public interface CalculatedStatus {
+        boolean isHealthy();
+        String getDescription();
+    }
+
+    private final class PublishJob implements Runnable {
+        @Override public void run() {
+            try {
+                executorTime = System.currentTimeMillis();
+                executorQueued.set(false);
+
+                publishNow();
+
+            } catch (Exception e) {
+                if (isRunning()) {
+                    LOG.error("Problem resizing: "+e, e);
+                } else {
+                    if (LOG.isDebugEnabled()) LOG.debug("Problem resizing, but no longer running: "+e, e);
+                }
+            } catch (Throwable t) {
+                LOG.error("Problem in service-failure-detector: "+t, t);
+                throw Exceptions.propagate(t);
+            }
+        }
+    }
+
+    private final class HealthPoller implements Runnable {
+        @Override
+        public void run() {
+            checkHealth();
+        }
+    }
+
+    private final class HealthPollingTaskFactory implements Callable<Task<?>> {
+        @Override
+        public Task<?> call() {
+            BasicTask<Void> task = new BasicTask<Void>(new HealthPoller());
+            BrooklynTaskTags.setTransient(task);
+            return task;
+        }
+    }
+
+    protected static class BasicCalculatedStatus implements CalculatedStatus {
+        private boolean healthy;
+        private String description;
+
+        public BasicCalculatedStatus(boolean healthy, String description) {
+            this.healthy = healthy;
+            this.description = description;
+        }
+
+        @Override
+        public boolean isHealthy() {
+            return healthy;
+        }
+
+        @Override
+        public String getDescription() {
+            return description;
+        }
+    }
+
+    public enum LastPublished {
+        NONE,
+        FAILED,
+        RECOVERED;
+    }
+
+    protected final AtomicReference<Long> stateLastGood = new AtomicReference<Long>();
+    protected final AtomicReference<Long> stateLastFail = new AtomicReference<Long>();
+
+    protected Long currentFailureStartTime = null;
+    protected Long currentRecoveryStartTime = null;
+
+    protected LastPublished lastPublished = LastPublished.NONE;
+
+    private final AtomicBoolean executorQueued = new AtomicBoolean(false);
+    private volatile long executorTime = 0;
+
+    private Callable<Task<?>> pollingTaskFactory = new HealthPollingTaskFactory();
+
+    private Task<?> scheduledTask;
+
+    protected abstract CalculatedStatus calculateStatus();
+
+    @Override
+    public void setEntity(EntityLocal entity) {
+        super.setEntity(entity);
+
+        if (isRunning()) {
+            doStartPolling();
+        }
+    }
+
+    @Override
+    public void suspend() {
+        scheduledTask.cancel(true);
+        super.suspend();
+    }
+
+    @Override
+    public void resume() {
+        currentFailureStartTime = null;
+        currentRecoveryStartTime = null;
+        lastPublished = LastPublished.NONE;
+        executorQueued.set(false);
+        executorTime = 0;
+
+        super.resume();
+        doStartPolling();
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void doStartPolling() {
+        if (scheduledTask == null || scheduledTask.isDone()) {
+            ScheduledTask task = new ScheduledTask(MutableMap.of("period", getPollPeriod(), "displayName", getTaskName()), pollingTaskFactory);
+            scheduledTask = ((EntityInternal)entity).getExecutionContext().submit(task);
+        }
+    }
+
+    private String getTaskName() {
+        return getDisplayName();
+    }
+
+    protected Duration getPollPeriod() {
+        return getConfig(POLL_PERIOD);
+    }
+
+    protected Duration getFailedStabilizationDelay() {
+        return getConfig(FAILED_STABILIZATION_DELAY);
+    }
+
+    protected Duration getRecoveredStabilizationDelay() {
+        return getConfig(RECOVERED_STABILIZATION_DELAY);
+    }
+
+    protected Sensor<FailureDescriptor> getSensorFailed() {
+        return getConfig(SENSOR_FAILED);
+    }
+
+    protected Sensor<FailureDescriptor> getSensorRecovered() {
+        return getConfig(SENSOR_RECOVERED);
+    }
+
+    private synchronized void checkHealth() {
+        CalculatedStatus status = calculateStatus();
+        boolean healthy = status.isHealthy();
+        long now = System.currentTimeMillis();
+
+        if (healthy) {
+            stateLastGood.set(now);
+            if (lastPublished == LastPublished.FAILED) {
+                if (currentRecoveryStartTime == null) {
+                    LOG.info("{} check for {}, now recovering: {}", new Object[] {this, entity, getDescription(status)});
+                    currentRecoveryStartTime = now;
+                    schedulePublish();
+                } else {
+                    if (LOG.isTraceEnabled()) LOG.trace("{} check for {}, continuing recovering: {}", new Object[] {this, entity, getDescription(status)});
+                }
+            } else {
+                if (currentFailureStartTime != null) {
+                    LOG.info("{} check for {}, now healthy: {}", new Object[] {this, entity, getDescription(status)});
+                    currentFailureStartTime = null;
+                } else {
+                    if (LOG.isTraceEnabled()) LOG.trace("{} check for {}, still healthy: {}", new Object[] {this, entity, getDescription(status)});
+                }
+            }
+        } else {
+            stateLastFail.set(now);
+            if (lastPublished != LastPublished.FAILED) {
+                if (currentFailureStartTime == null) {
+                    LOG.info("{} check for {}, now failing: {}", new Object[] {this, entity, getDescription(status)});
+                    currentFailureStartTime = now;
+                    schedulePublish();
+                } else {
+                    if (LOG.isTraceEnabled()) LOG.trace("{} check for {}, continuing failing: {}", new Object[] {this, entity, getDescription(status)});
+                }
+            } else {
+                if (currentRecoveryStartTime != null) {
+                    LOG.info("{} check for {}, now failing: {}", new Object[] {this, entity, getDescription(status)});
+                    currentRecoveryStartTime = null;
+                } else {
+                    if (LOG.isTraceEnabled()) LOG.trace("{} check for {}, still failed: {}", new Object[] {this, entity, getDescription(status)});
+                }
+            }
+        }
+    }
+
+    protected void schedulePublish() {
+        schedulePublish(0);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void schedulePublish(long delay) {
+        if (isRunning() && executorQueued.compareAndSet(false, true)) {
+            long now = System.currentTimeMillis();
+            delay = Math.max(0, Math.max(delay, (executorTime + MIN_PERIOD_BETWEEN_EXECS_MILLIS) - now));
+            if (LOG.isTraceEnabled()) LOG.trace("{} scheduling publish in {}ms", this, delay);
+
+            Runnable job = new PublishJob();
+
+            ScheduledTask task = new ScheduledTask(MutableMap.of("delay", Duration.of(delay, TimeUnit.MILLISECONDS)), new BasicTask<Void>(job));
+            ((EntityInternal)entity).getExecutionContext().submit(task);
+        }
+    }
+
+    private synchronized void publishNow() {
+        if (!isRunning()) return;
+
+        CalculatedStatus calculatedStatus = calculateStatus();
+        boolean healthy = calculatedStatus.isHealthy();
+
+        Long lastUpTime = stateLastGood.get();
+        Long lastDownTime = stateLastFail.get();
+        long serviceFailedStabilizationDelay = getFailedStabilizationDelay().toMilliseconds();
+        long serviceRecoveredStabilizationDelay = getRecoveredStabilizationDelay().toMilliseconds();
+        long now = System.currentTimeMillis();
+
+        if (healthy) {
+            if (lastPublished == LastPublished.FAILED) {
+                // only publish if consistently up for serviceRecoveredStabilizationDelay
+                long currentRecoveryPeriod = getTimeDiff(now, currentRecoveryStartTime);
+                long sinceLastDownPeriod = getTimeDiff(now, lastDownTime);
+                if (currentRecoveryPeriod > serviceRecoveredStabilizationDelay && sinceLastDownPeriod > serviceRecoveredStabilizationDelay) {
+                    String description = getDescription(calculatedStatus);
+                    LOG.warn("{} check for {}, publishing recovered: {}", new Object[] {this, entity, description});
+                    entity.emit(getSensorRecovered(), new HASensors.FailureDescriptor(entity, description));
+                    lastPublished = LastPublished.RECOVERED;
+                    currentFailureStartTime = null;
+                } else {
+                    long nextAttemptTime = Math.max(serviceRecoveredStabilizationDelay - currentRecoveryPeriod, serviceRecoveredStabilizationDelay - sinceLastDownPeriod);
+                    schedulePublish(nextAttemptTime);
+                }
+            }
+        } else {
+            if (lastPublished != LastPublished.FAILED) {
+                // only publish if consistently down for serviceFailedStabilizationDelay
+                long currentFailurePeriod = getTimeDiff(now, currentFailureStartTime);
+                long sinceLastUpPeriod = getTimeDiff(now, lastUpTime);
+                if (currentFailurePeriod > serviceFailedStabilizationDelay && sinceLastUpPeriod > serviceFailedStabilizationDelay) {
+                    String description = getDescription(calculatedStatus);
+                    LOG.warn("{} connectivity-check for {}, publishing failed: {}", new Object[] {this, entity, description});
+                    entity.emit(getSensorFailed(), new HASensors.FailureDescriptor(entity, description));
+                    lastPublished = LastPublished.FAILED;
+                    currentRecoveryStartTime = null;
+                } else {
+                    long nextAttemptTime = Math.max(serviceFailedStabilizationDelay - currentFailurePeriod, serviceFailedStabilizationDelay - sinceLastUpPeriod);
+                    schedulePublish(nextAttemptTime);
+                }
+            }
+        }
+    }
+
+    protected String getDescription(CalculatedStatus status) {
+        Long lastUpTime = stateLastGood.get();
+        Long lastDownTime = stateLastGood.get();
+        Duration serviceFailedStabilizationDelay = getFailedStabilizationDelay();
+        Duration serviceRecoveredStabilizationDelay = getRecoveredStabilizationDelay();
+
+        return String.format("%s; healthy=%s; timeNow=%s; lastUp=%s; lastDown=%s; lastPublished=%s; "+
+                    "currentFailurePeriod=%s; currentRecoveryPeriod=%s",
+                status.getDescription(),
+                status.isHealthy(),
+                Time.makeDateString(System.currentTimeMillis()),
+                (lastUpTime != null ? Time.makeDateString(lastUpTime) : "<never>"),
+                (lastDownTime != null ? Time.makeDateString(lastDownTime) : "<never>"),
+                lastPublished,
+                (currentFailureStartTime != null ? getTimeStringSince(currentFailureStartTime) : "<none>") + " (stabilization "+makeTimeStringRounded(serviceFailedStabilizationDelay) + ")",
+                (currentRecoveryStartTime != null ? getTimeStringSince(currentRecoveryStartTime) : "<none>") + " (stabilization "+makeTimeStringRounded(serviceRecoveredStabilizationDelay) + ")");
+    }
+
+    private long getTimeDiff(Long recent, Long previous) {
+        return (previous == null) ? recent : (recent - previous);
+    }
+
+    private String getTimeStringSince(Long time) {
+        return time == null ? null : Time.makeTimeStringRounded(System.currentTimeMillis() - time);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/ha/ConditionalSuspendPolicy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/ha/ConditionalSuspendPolicy.java b/policy/src/main/java/org/apache/brooklyn/policy/ha/ConditionalSuspendPolicy.java
new file mode 100644
index 0000000..4bc1dc1
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/ha/ConditionalSuspendPolicy.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.policy.Policy;
+import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.util.javalang.JavaClassNames;
+
+import com.google.common.base.Preconditions;
+
+public class ConditionalSuspendPolicy extends AbstractPolicy {
+    private static final Logger LOG = LoggerFactory.getLogger(ConditionalSuspendPolicy.class);
+
+    @SetFromFlag("suppressSensor")
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public static final ConfigKey<Sensor<?>> SUSPEND_SENSOR = (ConfigKey) ConfigKeys.newConfigKey(Sensor.class,
+            "suppressSensor", "Sensor which will suppress the target policy", HASensors.CONNECTION_FAILED); 
+
+    @SetFromFlag("resetSensor")
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public static final ConfigKey<Sensor<?>> RESUME_SENSOR = (ConfigKey) ConfigKeys.newConfigKey(Sensor.class,
+            "resetSensor", "Resume target policy when this sensor is observed", HASensors.CONNECTION_RECOVERED);
+
+    @SetFromFlag("target")
+    public static final ConfigKey<Object> SUSPEND_TARGET = ConfigKeys.newConfigKey(Object.class,
+            "target", "The target policy to suspend. Either direct reference or the value of the suspendTarget config on a policy from the same entity.");
+
+    @Override
+    public void setEntity(EntityLocal entity) {
+        super.setEntity(entity);
+        Object target = config().get(SUSPEND_TARGET);
+        Preconditions.checkNotNull(target, "Suspend target required");
+        Preconditions.checkNotNull(getTargetPolicy(), "Can't find target policy set in " + SUSPEND_TARGET.getName() + ": " + target);
+        subscribe();
+        uniqueTag = JavaClassNames.simpleClassName(getClass())+":"+getConfig(SUSPEND_SENSOR).getName()+":"+getConfig(RESUME_SENSOR).getName();
+    }
+
+    private void subscribe() {
+        subscribe(entity, getConfig(SUSPEND_SENSOR), new SensorEventListener<Object>() {
+            @Override public void onEvent(final SensorEvent<Object> event) {
+                if (isRunning()) {
+                    Policy target = getTargetPolicy();
+                    target.suspend();
+                    LOG.debug("Suspended policy " + target + ", triggered by " + event.getSensor() + " = " + event.getValue());
+                }
+            }
+
+        });
+        subscribe(entity, getConfig(RESUME_SENSOR), new SensorEventListener<Object>() {
+            @Override public void onEvent(final SensorEvent<Object> event) {
+                if (isRunning()) {
+                    Policy target = getTargetPolicy();
+                    target.resume();
+                    LOG.debug("Resumed policy " + target + ", triggered by " + event.getSensor() + " = " + event.getValue());
+                }
+            }
+        });
+    }
+
+    private Policy getTargetPolicy() {
+        Object target = config().get(SUSPEND_TARGET);
+        if (target instanceof Policy) {
+            return (Policy)target;
+        } else if (target instanceof String) {
+            for (Policy policy : entity.getPolicies()) {
+                // No way to set config values for keys NOT declared in the policy,
+                // so must use displayName as a generally available config value.
+                if (target.equals(policy.getDisplayName()) || target.equals(policy.getClass().getName())) {
+                    return policy;
+                }
+            }
+        } else {
+            throw new IllegalStateException("Unexpected type " + target.getClass() + " for target " + target);
+        }
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/ha/ConnectionFailureDetector.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/ha/ConnectionFailureDetector.java b/policy/src/main/java/org/apache/brooklyn/policy/ha/ConnectionFailureDetector.java
new file mode 100644
index 0000000..2cca77f
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/ha/ConnectionFailureDetector.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import org.apache.brooklyn.api.catalog.Catalog;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.event.basic.BasicNotificationSensor;
+
+import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
+
+import brooklyn.util.guava.Maybe;
+import brooklyn.util.net.Networking;
+import brooklyn.util.time.Duration;
+
+import com.google.common.net.HostAndPort;
+
+/**
+ * Monitors a given {@link HostAndPort}, to emit HASensors.CONNECTION_FAILED and HASensors.CONNECTION_RECOVERED 
+ * if the connection is lost/restored.
+ */
+@Catalog(name="Connection Failure Detector", description="HA policy for monitoring a host:port, "
+        + "emitting an event if the connection is lost/restored")
+public class ConnectionFailureDetector extends AbstractFailureDetector {
+
+    public static final ConfigKey<HostAndPort> ENDPOINT = ConfigKeys.newConfigKey(HostAndPort.class, "connectionFailureDetector.endpoint");
+
+    public static final ConfigKey<Duration> POLL_PERIOD = ConfigKeys.newConfigKey(Duration.class, "connectionFailureDetector.pollPeriod", "", Duration.ONE_SECOND);
+
+    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_FAILED = HASensors.CONNECTION_FAILED;
+
+    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_RECOVERED = HASensors.CONNECTION_RECOVERED;
+
+    @SetFromFlag("connectionFailedStabilizationDelay")
+    public static final ConfigKey<Duration> CONNECTION_FAILED_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
+            .name("connectionFailureDetector.serviceFailedStabilizationDelay")
+            .description("Time period for which the connection must be consistently down for "
+                    + "(e.g. doesn't report down-up-down) before concluding failure. "
+                    + "Note that long TCP timeouts mean there can be long (e.g. 70 second) "
+                    + "delays in noticing a connection refused condition.")
+            .defaultValue(Duration.ZERO)
+            .build();
+
+    @SetFromFlag("connectionRecoveredStabilizationDelay")
+    public static final ConfigKey<Duration> CONNECTION_RECOVERED_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
+            .name("connectionFailureDetector.serviceRecoveredStabilizationDelay")
+            .description("For a failed connection, time period for which the connection must be consistently up for (e.g. doesn't report up-down-up) before concluding recovered")
+            .defaultValue(Duration.ZERO)
+            .build();
+
+    @Override
+    public void init() {
+        super.init();
+        getRequiredConfig(ENDPOINT); // just to confirm it's set, failing fast
+        if (config().getRaw(SENSOR_FAILED).isAbsent()) {
+            config().set(SENSOR_FAILED, CONNECTION_FAILED);
+        }
+        if (config().getRaw(SENSOR_RECOVERED).isAbsent()) {
+            config().set(SENSOR_RECOVERED, CONNECTION_RECOVERED);
+        }
+    }
+
+    @Override
+    protected CalculatedStatus calculateStatus() {
+        HostAndPort endpoint = getConfig(ENDPOINT);
+        boolean isHealthy = Networking.isReachable(endpoint);
+        return new BasicCalculatedStatus(isHealthy, "endpoint=" + endpoint);
+    }
+
+    //Persistence compatibility overrides
+    @Override
+    protected Duration getPollPeriod() {
+        return getConfig(POLL_PERIOD);
+    }
+
+    @Override
+    protected Duration getFailedStabilizationDelay() {
+        return getConfig(CONNECTION_FAILED_STABILIZATION_DELAY);
+    }
+
+    @Override
+    protected Duration getRecoveredStabilizationDelay() {
+        return getConfig(CONNECTION_RECOVERED_STABILIZATION_DELAY);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected Sensor<FailureDescriptor> getSensorFailed() {
+        Maybe<Object> sensorFailed = config().getRaw(SENSOR_FAILED);
+        if (sensorFailed.isPresent()) {
+            return (Sensor<FailureDescriptor>)sensorFailed.get();
+        } else {
+            return CONNECTION_FAILED;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected Sensor<FailureDescriptor> getSensorRecovered() {
+        Maybe<Object> sensorRecovered = config().getRaw(SENSOR_RECOVERED);
+        if (sensorRecovered.isPresent()) {
+            return (Sensor<FailureDescriptor>)sensorRecovered.get();
+        } else {
+            return CONNECTION_RECOVERED;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/ha/HASensors.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/ha/HASensors.java b/policy/src/main/java/org/apache/brooklyn/policy/ha/HASensors.java
new file mode 100644
index 0000000..24763f0
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/ha/HASensors.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.ha;
+
+import brooklyn.event.basic.BasicNotificationSensor;
+
+import com.google.common.base.Objects;
+
+public class HASensors {
+
+    public static final BasicNotificationSensor<FailureDescriptor> ENTITY_FAILED = new BasicNotificationSensor<FailureDescriptor>(
+            FailureDescriptor.class, "ha.entityFailed", "Indicates that an entity has failed");
+    
+    public static final BasicNotificationSensor<FailureDescriptor> ENTITY_RECOVERED = new BasicNotificationSensor<FailureDescriptor>(
+            FailureDescriptor.class, "ha.entityRecovered", "Indicates that a previously failed entity has recovered");
+    
+    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_FAILED = new BasicNotificationSensor<FailureDescriptor>(
+            FailureDescriptor.class, "ha.connectionFailed", "Indicates that a connection has failed");
+    
+    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_RECOVERED = new BasicNotificationSensor<FailureDescriptor>(
+            FailureDescriptor.class, "ha.connectionRecovered", "Indicates that a previously failed connection has recovered");
+    
+    // TODO How to make this serializable with the entity reference
+    public static class FailureDescriptor {
+        private final Object component;
+        private final String description;
+        
+        public FailureDescriptor(Object component, String description) {
+            this.component = component;
+            this.description = description;
+        }
+        
+        public Object getComponent() {
+            return component;
+        }
+        
+        public String getDescription() {
+            return description;
+        }
+        
+        @Override
+        public String toString() {
+            return Objects.toStringHelper(this).add("component", component).add("description", description).toString();
+        }
+    }
+}


[10/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/ha/ServiceReplacerTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/ha/ServiceReplacerTest.java b/policy/src/test/java/brooklyn/policy/ha/ServiceReplacerTest.java
deleted file mode 100644
index c679224..0000000
--- a/policy/src/test/java/brooklyn/policy/ha/ServiceReplacerTest.java
+++ /dev/null
@@ -1,340 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertNotEquals;
-import static org.testng.Assert.assertTrue;
-
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.api.location.LocationSpec;
-import org.apache.brooklyn.api.management.ManagementContext;
-import org.apache.brooklyn.api.policy.PolicySpec;
-import org.apache.brooklyn.core.util.config.ConfigBag;
-import org.apache.brooklyn.test.EntityTestUtils;
-import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.apache.brooklyn.test.entity.TestEntity;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.Assert;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.ApplicationBuilder;
-import brooklyn.entity.basic.Attributes;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.basic.EntityInternal;
-import brooklyn.entity.basic.Lifecycle;
-import brooklyn.entity.basic.QuorumCheck;
-import brooklyn.entity.basic.ServiceStateLogic.ComputeServiceIndicatorsFromChildrenAndMembers;
-import brooklyn.entity.group.DynamicCluster;
-import brooklyn.entity.trait.FailingEntity;
-
-import org.apache.brooklyn.location.basic.SimulatedLocation;
-
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.test.Asserts;
-import brooklyn.util.javalang.JavaClassNames;
-
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-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.collect.Sets;
-
-public class ServiceReplacerTest {
-
-    private static final Logger log = LoggerFactory.getLogger(ServiceReplacerTest.class);
-    
-    private ManagementContext managementContext;
-    private TestApplication app;
-    private SimulatedLocation loc;
-    private SensorEventListener<Object> eventListener;
-    private List<SensorEvent<?>> events;
-    
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        managementContext = new LocalManagementContextForTests();
-        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
-        loc = managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
-        events = Lists.newCopyOnWriteArrayList();
-        eventListener = new SensorEventListener<Object>() {
-            @Override public void onEvent(SensorEvent<Object> event) {
-                events.add(event);
-            }
-        };
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        if (managementContext != null) Entities.destroyAll(managementContext);
-    }
-    
-    @Test
-    public void testReplacesFailedMember() throws Exception {
-        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(TestEntity.class))
-                .configure(DynamicCluster.INITIAL_SIZE, 3));
-        app.start(ImmutableList.<Location>of(loc));
-
-        ServiceReplacer policy = new ServiceReplacer(new ConfigBag().configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
-        cluster.addPolicy(policy);
-
-        final Set<Entity> initialMembers = ImmutableSet.copyOf(cluster.getMembers());
-        final TestEntity e1 = (TestEntity) Iterables.get(initialMembers, 1);
-        
-        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
-        
-        // Expect e1 to be replaced
-        Asserts.succeedsEventually(new Runnable() {
-            @Override public void run() {
-                Set<Entity> newMembers = Sets.difference(ImmutableSet.copyOf(cluster.getMembers()), initialMembers);
-                Set<Entity> removedMembers = Sets.difference(initialMembers, ImmutableSet.copyOf(cluster.getMembers()));
-                assertEquals(removedMembers, ImmutableSet.of(e1));
-                assertEquals(newMembers.size(), 1);
-                assertEquals(((TestEntity)Iterables.getOnlyElement(newMembers)).getCallHistory(), ImmutableList.of("start"));
-                assertEquals(e1.getCallHistory(), ImmutableList.of("start", "stop"));
-                assertFalse(Entities.isManaged(e1));
-            }});
-    }
-
-    @Test(invocationCount=100)
-    public void testSetsOnFireWhenFailToReplaceMemberManyTimes() throws Exception {
-        testSetsOnFireWhenFailToReplaceMember();
-    }
-    
-    // fails the startup of the replacement entity (but not the original). 
-    @Test
-    public void testSetsOnFireWhenFailToReplaceMember() throws Exception {
-        app.subscribe(null, ServiceReplacer.ENTITY_REPLACEMENT_FAILED, eventListener);
-        
-        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(FailingEntity.class)
-                        .configure(FailingEntity.FAIL_ON_START_CONDITION, predicateOnlyTrueForCallAtOrAfter(2)))
-                .configure(DynamicCluster.INITIAL_SIZE, 1)
-                .configure(DynamicCluster.QUARANTINE_FAILED_ENTITIES, true)
-                .configure(ComputeServiceIndicatorsFromChildrenAndMembers.UP_QUORUM_CHECK, QuorumCheck.QuorumChecks.alwaysTrue())
-                .configure(ComputeServiceIndicatorsFromChildrenAndMembers.RUNNING_QUORUM_CHECK, QuorumCheck.QuorumChecks.alwaysTrue()));
-        app.start(ImmutableList.<Location>of(loc));
-        
-        // should not be on fire
-        Assert.assertNotEquals(cluster.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
-        // and should eventually be running
-        EntityTestUtils.assertAttributeEqualsEventually(cluster, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
-        
-        log.info("started "+app+" for "+JavaClassNames.niceClassAndMethod());
-        
-        ServiceReplacer policy = new ServiceReplacer(new ConfigBag().configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
-        cluster.addPolicy(policy);
-        
-        final Set<Entity> initialMembers = ImmutableSet.copyOf(cluster.getMembers());
-        final TestEntity e1 = (TestEntity) Iterables.get(initialMembers, 0);
-        
-        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
-
-        // Expect cluster to go on-fire when fails to start replacement
-        // Note that we've set up-quorum and running-quorum to be "alwaysTrue" so that we don't get a transient onFire
-        // when the failed node fails to start (but before it has been removed from the group to be put in quarantine).
-        EntityTestUtils.assertAttributeEqualsEventually(cluster, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-
-        // Expect to have the second failed entity still kicking around as proof (in quarantine)
-        // The cluster should NOT go on fire until after the 2nd failure
-        Iterable<Entity> members = Iterables.filter(managementContext.getEntityManager().getEntities(), Predicates.instanceOf(FailingEntity.class));
-        assertEquals(Iterables.size(members), 2);
-
-        // e2 failed to start, so it won't have called stop on e1
-        TestEntity e2 = (TestEntity) Iterables.getOnlyElement(Sets.difference(ImmutableSet.copyOf(members), initialMembers));
-        assertEquals(e1.getCallHistory(), ImmutableList.of("start"), "e1.history="+e1.getCallHistory());
-        assertEquals(e2.getCallHistory(), ImmutableList.of("start"), "e2.history="+e2.getCallHistory());
-
-        // And will have received notification event about it
-        assertEventuallyHasEntityReplacementFailedEvent(cluster);
-    }
-    
-    @Test(groups="Integration") // has a 1 second wait
-    public void testDoesNotOnFireWhenFailToReplaceMember() throws Exception {
-        app.subscribe(null, ServiceReplacer.ENTITY_REPLACEMENT_FAILED, eventListener);
-        
-        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(FailingEntity.class)
-                        .configure(FailingEntity.FAIL_ON_START_CONDITION, predicateOnlyTrueForCallAtOrAfter(2)))
-                .configure(DynamicCluster.INITIAL_SIZE, 1)
-                .configure(DynamicCluster.QUARANTINE_FAILED_ENTITIES, true));
-        app.start(ImmutableList.<Location>of(loc));
-        
-        ServiceReplacer policy = new ServiceReplacer(new ConfigBag()
-                .configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED)
-                .configure(ServiceReplacer.SET_ON_FIRE_ON_FAILURE, false));
-        cluster.addPolicy(policy);
-        
-        final Set<Entity> initialMembers = ImmutableSet.copyOf(cluster.getMembers());
-        final TestEntity e1 = (TestEntity) Iterables.get(initialMembers, 0);
-        
-        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
-
-        // Configured to not mark cluster as on fire
-        Asserts.succeedsContinually(new Runnable() {
-            @Override public void run() {
-                assertNotEquals(cluster.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
-            }});
-        
-        // And will have received notification event about it
-        assertEventuallyHasEntityReplacementFailedEvent(cluster);
-    }
-
-    @Test(groups="Integration")  // 1s wait
-    public void testStopFailureOfOldEntityDoesNotSetClusterOnFire() throws Exception {
-        app.subscribe(null, ServiceReplacer.ENTITY_REPLACEMENT_FAILED, eventListener);
-        
-        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(FailingEntity.class)
-                        .configure(FailingEntity.FAIL_ON_STOP_CONDITION, predicateOnlyTrueForCallAt(1)))
-                .configure(DynamicCluster.INITIAL_SIZE, 2));
-        app.start(ImmutableList.<Location>of(loc));
-        
-        cluster.addPolicy(PolicySpec.create(ServiceReplacer.class)
-                .configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
-        
-        final Set<Entity> initialMembers = ImmutableSet.copyOf(cluster.getMembers());
-        final TestEntity e1 = (TestEntity) Iterables.get(initialMembers, 0);
-        
-        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
-
-        // Expect e1 to be replaced
-        Asserts.succeedsEventually(new Runnable() {
-            @Override public void run() {
-                Set<Entity> newMembers = Sets.difference(ImmutableSet.copyOf(cluster.getMembers()), initialMembers);
-                Set<Entity> removedMembers = Sets.difference(initialMembers, ImmutableSet.copyOf(cluster.getMembers()));
-                assertEquals(removedMembers, ImmutableSet.of(e1));
-                assertEquals(newMembers.size(), 1);
-                assertEquals(((TestEntity)Iterables.getOnlyElement(newMembers)).getCallHistory(), ImmutableList.of("start"));
-                assertEquals(e1.getCallHistory(), ImmutableList.of("start", "stop"));
-                assertFalse(Entities.isManaged(e1));
-            }});
-
-        // Failure to stop the failed member should not cause "on-fire" of cluster
-        Asserts.succeedsContinually(new Runnable() {
-            @Override public void run() {
-                assertNotEquals(cluster.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
-            }});
-    }
-
-    /**
-     * If we keep on getting failure reports, never managing to replace the failed node, then don't keep trying
-     * (i.e. avoid infinite loop).
-     * 
-     * TODO This code + configuration needs some work; it's not testing quite the scenarios that I
-     * was thinking of!
-     * I saw problem where a node failed, and the replacements failed, and we ended up trying thousands of times.
-     * (describing this scenario is made more complex by me having temporarily disabled the cluster from 
-     * removing failed members, for debugging purposes!)
-     * Imagine these two scenarios:
-     * <ol>
-     *   <li>Entity fails during call to start().
-     *       Here, the cluster removes it as a member (either unmanages it or puts it in quarantine)
-     *       So the ENTITY_FAILED is ignored because the entity is not a member at that point.
-     *   <li>Entity returns from start(), but quickly goes to service-down.
-     *       Here we'll keep trying to replace that entity. Depending how long that takes, we'll either 
-     *       enter a horrible infinite loop, or we'll just provision a huge number of VMs over a long 
-     *       time period.
-     *       Unfortunately this scenario is not catered for in the code yet.
-     * </ol>
-     */
-    @Test(groups="Integration") // because takes 1.2 seconds
-    public void testAbandonsReplacementAfterNumFailures() throws Exception {
-        app.subscribe(null, ServiceReplacer.ENTITY_REPLACEMENT_FAILED, eventListener);
-        
-        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(FailingEntity.class)
-                        .configure(FailingEntity.FAIL_ON_START_CONDITION, predicateOnlyTrueForCallAtOrAfter(11)))
-                .configure(DynamicCluster.INITIAL_SIZE, 10)
-                .configure(DynamicCluster.QUARANTINE_FAILED_ENTITIES, true));
-        app.start(ImmutableList.<Location>of(loc));
-        
-        ServiceReplacer policy = new ServiceReplacer(new ConfigBag()
-                .configure(ServiceReplacer.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED)
-                .configure(ServiceReplacer.FAIL_ON_NUM_RECURRING_FAILURES, 3));
-        cluster.addPolicy(policy);
-
-        final Set<Entity> initialMembers = ImmutableSet.copyOf(cluster.getMembers());
-        for (int i = 0; i < 5; i++) {
-            final int counter = i+1;
-            EntityInternal entity = (EntityInternal) Iterables.get(initialMembers, i);
-            entity.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(entity, "simulate failure"));
-            if (i <= 3) {
-                Asserts.succeedsEventually(new Runnable() {
-                    @Override public void run() {
-                        Set<FailingEntity> all = ImmutableSet.copyOf(Iterables.filter(managementContext.getEntityManager().getEntities(), FailingEntity.class));
-                        Set<FailingEntity> replacements = Sets.difference(all, initialMembers);
-                        Set<?> replacementMembers = Sets.intersection(ImmutableSet.of(cluster.getMembers()), replacements);
-                        assertTrue(replacementMembers.isEmpty());
-                        assertEquals(replacements.size(), counter);
-                    }});
-            } else {
-                Asserts.succeedsContinually(new Runnable() {
-                    @Override public void run() {
-                        Set<FailingEntity> all = ImmutableSet.copyOf(Iterables.filter(managementContext.getEntityManager().getEntities(), FailingEntity.class));
-                        Set<FailingEntity> replacements = Sets.difference(all, initialMembers);
-                        assertEquals(replacements.size(), 4);
-                    }});
-            }
-        }
-    }
-
-
-    private Predicate<Object> predicateOnlyTrueForCallAt(final int callNumber) {
-        return predicateOnlyTrueForCallRange(callNumber, callNumber);
-    }
-
-    private Predicate<Object> predicateOnlyTrueForCallAtOrAfter(final int callLowerNumber) {
-        return predicateOnlyTrueForCallRange(callLowerNumber, Integer.MAX_VALUE);
-    }
-    
-    private Predicate<Object> predicateOnlyTrueForCallRange(final int callLowerNumber, final int callUpperNumber) {
-        return new Predicate<Object>() {
-            private final AtomicInteger counter = new AtomicInteger(0);
-            @Override public boolean apply(Object input) {
-                int num = counter.incrementAndGet();
-                return num >= callLowerNumber && num <= callUpperNumber;
-            }
-        };
-    }
-
-    private void assertEventuallyHasEntityReplacementFailedEvent(final Entity expectedCluster) {
-        Asserts.succeedsEventually(new Runnable() {
-            @Override public void run() {
-                assertEquals(Iterables.getOnlyElement(events).getSensor(), ServiceReplacer.ENTITY_REPLACEMENT_FAILED, "events="+events);
-                assertEquals(Iterables.getOnlyElement(events).getSource(), expectedCluster, "events="+events);
-                assertEquals(((FailureDescriptor)Iterables.getOnlyElement(events).getValue()).getComponent(), expectedCluster, "events="+events);
-            }});
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/ha/ServiceRestarterTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/ha/ServiceRestarterTest.java b/policy/src/test/java/brooklyn/policy/ha/ServiceRestarterTest.java
deleted file mode 100644
index 81cff78..0000000
--- a/policy/src/test/java/brooklyn/policy/ha/ServiceRestarterTest.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotEquals;
-import static org.testng.Assert.assertTrue;
-
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.api.management.ManagementContext;
-import org.apache.brooklyn.api.policy.PolicySpec;
-import org.apache.brooklyn.core.util.config.ConfigBag;
-import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.apache.brooklyn.test.entity.TestEntity;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.ApplicationBuilder;
-import brooklyn.entity.basic.Attributes;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.basic.Lifecycle;
-import brooklyn.entity.trait.FailingEntity;
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.test.Asserts;
-import brooklyn.util.exceptions.Exceptions;
-
-import com.google.common.base.Function;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-
-public class ServiceRestarterTest {
-
-    private static final int TIMEOUT_MS = 10*1000;
-
-    private ManagementContext managementContext;
-    private TestApplication app;
-    private TestEntity e1;
-    private ServiceRestarter policy;
-    private SensorEventListener<Object> eventListener;
-    private List<SensorEvent<?>> events;
-    
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        managementContext = new LocalManagementContextForTests();
-        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
-        e1 = app.createAndManageChild(EntitySpec.create(TestEntity.class));
-        events = Lists.newCopyOnWriteArrayList();
-        eventListener = new SensorEventListener<Object>() {
-            @Override public void onEvent(SensorEvent<Object> event) {
-                events.add(event);
-            }
-        };
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        if (managementContext != null) Entities.destroyAll(managementContext);
-    }
-    
-    @Test
-    public void testRestartsOnFailure() throws Exception {
-        policy = new ServiceRestarter(new ConfigBag().configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
-        e1.addPolicy(policy);
-        
-        e1.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
-        
-        Asserts.succeedsEventually(new Runnable() {
-            @Override public void run() {
-                assertEquals(e1.getCallHistory(), ImmutableList.of("restart"));
-            }});
-    }
-    
-    @Test(groups="Integration") // Has a 1 second wait
-    public void testDoesNotRestartsWhenHealthy() throws Exception {
-        policy = new ServiceRestarter(new ConfigBag().configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
-        e1.addPolicy(policy);
-        
-        e1.emit(HASensors.ENTITY_RECOVERED, new FailureDescriptor(e1, "not a failure"));
-        
-        Asserts.succeedsContinually(new Runnable() {
-            @Override public void run() {
-                assertEquals(e1.getCallHistory(), ImmutableList.of());
-            }});
-    }
-    
-    @Test
-    public void testEmitsFailureEventWhenRestarterFails() throws Exception {
-        final FailingEntity e2 = app.createAndManageChild(EntitySpec.create(FailingEntity.class)
-                .configure(FailingEntity.FAIL_ON_RESTART, true));
-        app.subscribe(e2, ServiceRestarter.ENTITY_RESTART_FAILED, eventListener);
-
-        policy = new ServiceRestarter(new ConfigBag().configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
-        e2.addPolicy(policy);
-
-        e2.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e2, "simulate failure"));
-        
-        Asserts.succeedsEventually(new Runnable() {
-            @Override public void run() {
-                assertEquals(Iterables.getOnlyElement(events).getSensor(), ServiceRestarter.ENTITY_RESTART_FAILED, "events="+events);
-                assertEquals(Iterables.getOnlyElement(events).getSource(), e2, "events="+events);
-                assertEquals(((FailureDescriptor)Iterables.getOnlyElement(events).getValue()).getComponent(), e2, "events="+events);
-            }});
-        
-        assertEquals(e2.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
-    }
-    
-    @Test
-    public void testDoesNotSetOnFireOnFailure() throws Exception {
-        final FailingEntity e2 = app.createAndManageChild(EntitySpec.create(FailingEntity.class)
-                .configure(FailingEntity.FAIL_ON_RESTART, true));
-        app.subscribe(e2, ServiceRestarter.ENTITY_RESTART_FAILED, eventListener);
-
-        policy = new ServiceRestarter(new ConfigBag()
-                .configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED)
-                .configure(ServiceRestarter.SET_ON_FIRE_ON_FAILURE, false));
-        e2.addPolicy(policy);
-
-        e2.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e2, "simulate failure"));
-        
-        Asserts.succeedsContinually(new Runnable() {
-            @Override public void run() {
-                assertNotEquals(e2.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
-            }});
-    }
-    
-    // Previously RestarterPolicy called entity.restart inside the event-listener thread.
-    // That caused all other events for that entity's subscriptions to be queued until that
-    // entity's single event handler thread was free again.
-    @Test
-    public void testRestartDoesNotBlockOtherSubscriptions() throws Exception {
-        final CountDownLatch inRestartLatch = new CountDownLatch(1);
-        final CountDownLatch continueRestartLatch = new CountDownLatch(1);
-        
-        final FailingEntity e2 = app.createAndManageChild(EntitySpec.create(FailingEntity.class)
-                .configure(FailingEntity.FAIL_ON_RESTART, true)
-                .configure(FailingEntity.EXEC_ON_FAILURE, new Function<Object, Void>() {
-                    @Override public Void apply(Object input) {
-                        inRestartLatch.countDown();
-                        try {
-                            continueRestartLatch.await();
-                        } catch (InterruptedException e) {
-                            throw Exceptions.propagate(e);
-                        }
-                        return null;
-                    }}));
-        
-        e2.addPolicy(PolicySpec.create(ServiceRestarter.class)
-                .configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
-        e2.subscribe(e2, TestEntity.SEQUENCE, eventListener);
-
-        // Cause failure, and wait for entity.restart to be blocking
-        e2.emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
-        assertTrue(inRestartLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-        
-        // Expect other notifications to continue to get through
-        e2.setAttribute(TestEntity.SEQUENCE, 1);
-        Asserts.succeedsEventually(new Runnable() {
-            @Override public void run() {
-                assertEquals(Iterables.getOnlyElement(events).getValue(), 1);
-            }});
-
-        // Allow restart to finish
-        continueRestartLatch.countDown();
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/AbstractLoadBalancingPolicyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/AbstractLoadBalancingPolicyTest.java b/policy/src/test/java/brooklyn/policy/loadbalancing/AbstractLoadBalancingPolicyTest.java
deleted file mode 100644
index 68db44c..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/AbstractLoadBalancingPolicyTest.java
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import static org.testng.Assert.assertEquals;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.Set;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.event.AttributeSensor;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.DynamicGroup;
-import brooklyn.entity.basic.Entities;
-import brooklyn.event.basic.BasicConfigKey;
-import brooklyn.event.basic.Sensors;
-import org.apache.brooklyn.location.basic.SimulatedLocation;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.time.Time;
-
-import com.google.common.base.Function;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-
-public class AbstractLoadBalancingPolicyTest {
-    
-    private static final Logger LOG = LoggerFactory.getLogger(AbstractLoadBalancingPolicyTest.class);
-    
-    protected static final long TIMEOUT_MS = 10*1000;
-    protected static final long SHORT_WAIT_MS = 250;
-    
-    protected static final long CONTAINER_STARTUP_DELAY_MS = 100;
-    
-    public static final AttributeSensor<Integer> TEST_METRIC =
-        Sensors.newIntegerSensor("test.metric", "Dummy workrate for test entities");
-    
-    public static final ConfigKey<Double> LOW_THRESHOLD_CONFIG_KEY = new BasicConfigKey<Double>(Double.class, TEST_METRIC.getName()+".threshold.low", "desc", 0.0);
-    public static final ConfigKey<Double> HIGH_THRESHOLD_CONFIG_KEY = new BasicConfigKey<Double>(Double.class, TEST_METRIC.getName()+".threshold.high", "desc", 0.0);
-    
-    protected TestApplication app;
-    protected SimulatedLocation loc;
-    protected BalanceableWorkerPool pool;
-    protected DefaultBalanceablePoolModel<Entity, Entity> model;
-    protected LoadBalancingPolicy policy;
-    protected Group containerGroup;
-    protected Group itemGroup;
-    protected Random random = new Random();
-    
-    @BeforeMethod(alwaysRun=true)
-    public void before() {
-        LOG.debug("In AbstractLoadBalancingPolicyTest.before()");
-        
-        MockItemEntityImpl.totalMoveCount.set(0);
-        MockItemEntityImpl.lastMoveTime.set(0);
-        
-        loc = new SimulatedLocation(MutableMap.of("name", "loc"));
-        
-        model = new DefaultBalanceablePoolModel<Entity, Entity>("pool-model");
-        
-        app = TestApplication.Factory.newManagedInstanceForTests();
-        containerGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
-                .displayName("containerGroup")
-                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockContainerEntity.class)));
-        itemGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
-                .displayName("itemGroup")
-                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockItemEntity.class)));
-        pool = app.createAndManageChild(EntitySpec.create(BalanceableWorkerPool.class));
-        pool.setContents(containerGroup, itemGroup);
-        policy = new LoadBalancingPolicy(MutableMap.of("minPeriodBetweenExecs", 1), TEST_METRIC, model);
-        pool.addPolicy(policy);
-        app.start(ImmutableList.of(loc));
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void after() {
-        if (policy != null) policy.destroy();
-        if (app != null) Entities.destroyAll(app.getManagementContext());
-    }
-    
-    // Using this utility, as it gives more info about the workrates of all containers rather than just the one that differs    
-    protected void assertWorkrates(Collection<MockContainerEntity> containers, Collection<Double> expectedC, double precision) {
-        Iterable<Double> actual = Iterables.transform(containers, new Function<MockContainerEntity, Double>() {
-            public Double apply(MockContainerEntity input) {
-                return getContainerWorkrate(input);
-            }});
-        
-        List<Double> expected = Lists.newArrayList(expectedC);
-        String errMsg = "actual="+actual+"; expected="+expected;
-        assertEquals(containers.size(), expected.size(), errMsg);
-        for (int i = 0; i < containers.size(); i++) {
-            assertEquals(Iterables.get(actual, i), expected.get(i), precision, errMsg);
-        }
-    }
-    
-    protected void assertWorkratesEventually(Collection<MockContainerEntity> containers, Iterable<? extends Movable> items, Collection<Double> expected) {
-        assertWorkratesEventually(containers, items, expected, 0d);
-    }
-
-    /**
-     * Asserts that the given container have the given expected workrates (by querying the containers directly).
-     * Accepts an accuracy of "precision" for each container's workrate.
-     */
-    protected void assertWorkratesEventually(final Collection<MockContainerEntity> containers, final Iterable<? extends Movable> items, final Collection<Double> expected, final double precision) {
-        try {
-            Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-                public void run() {
-                    assertWorkrates(containers, expected, precision);
-                }});
-        } catch (AssertionError e) {
-            String errMsg = e.getMessage()+"; "+verboseDumpToString(containers, items);
-            throw new RuntimeException(errMsg, e);
-        }
-    }
-
-    // Using this utility, as it gives more info about the workrates of all containers rather than just the one that differs    
-    protected void assertWorkratesContinually(List<MockContainerEntity> containers, Iterable<? extends Movable> items, List<Double> expected) {
-        assertWorkratesContinually(containers, items, expected, 0d);
-    }
-
-    /**
-     * Asserts that the given containers have the given expected workrates (by querying the containers directly)
-     * continuously for SHORT_WAIT_MS.
-     * Accepts an accuracy of "precision" for each container's workrate.
-     */
-    protected void assertWorkratesContinually(final List<MockContainerEntity> containers, Iterable<? extends Movable> items, final List<Double> expected, final double precision) {
-        try {
-            Asserts.succeedsContinually(MutableMap.of("timeout", SHORT_WAIT_MS), new Runnable() {
-                public void run() {
-                    assertWorkrates(containers, expected, precision);
-                }});
-        } catch (AssertionError e) {
-            String errMsg = e.getMessage()+"; "+verboseDumpToString(containers, items);
-            throw new RuntimeException(errMsg, e);
-        }
-    }
-
-    protected String verboseDumpToString(Iterable<MockContainerEntity> containers, Iterable<? extends Movable> items) {
-        Iterable<Double> containerRates = Iterables.transform(containers, new Function<MockContainerEntity, Double>() {
-            @Override public Double apply(MockContainerEntity input) {
-                return (double) input.getWorkrate();
-            }});
-        
-        Map<MockContainerEntity, Set<Movable>> itemDistributionByContainer = Maps.newLinkedHashMap();
-        for (MockContainerEntity container : containers) {
-            itemDistributionByContainer.put(container, container.getBalanceableItems());
-        }
-        
-        Map<Movable, BalanceableContainer<?>> itemDistributionByItem = Maps.newLinkedHashMap();
-        for (Movable item : items) {
-            itemDistributionByItem.put(item, item.getAttribute(Movable.CONTAINER));
-        }
-
-        String modelItemDistribution = model.itemDistributionToString();
-        return "containers="+containers+"; containerRates="+containerRates
-                +"; itemDistributionByContainer="+itemDistributionByContainer
-                +"; itemDistributionByItem="+itemDistributionByItem
-                +"; model="+modelItemDistribution
-                +"; totalMoves="+MockItemEntityImpl.totalMoveCount
-                +"; lastMoveTime="+Time.makeDateString(MockItemEntityImpl.lastMoveTime.get());
-    }
-    
-    protected MockContainerEntity newContainer(TestApplication app, String name, double lowThreshold, double highThreshold) {
-        return newAsyncContainer(app, name, lowThreshold, highThreshold, 0);
-    }
-    
-    /**
-     * Creates a new container that will take "delay" millis to complete its start-up.
-     */
-    protected MockContainerEntity newAsyncContainer(TestApplication app, String name, double lowThreshold, double highThreshold, long delay) {
-        MockContainerEntity container = app.createAndManageChild(EntitySpec.create(MockContainerEntity.class)
-                .displayName(name)
-                .configure(MockContainerEntity.DELAY, delay)
-                .configure(LOW_THRESHOLD_CONFIG_KEY, lowThreshold)
-                .configure(HIGH_THRESHOLD_CONFIG_KEY, highThreshold));
-        LOG.debug("Managed new container {}", container);
-        container.start(ImmutableList.of(loc));
-        return container;
-    }
-    
-    protected static MockItemEntity newItem(TestApplication app, MockContainerEntity container, String name, double workrate) {
-        MockItemEntity item = app.createAndManageChild(EntitySpec.create(MockItemEntity.class)
-                .displayName(name));
-        LOG.debug("Managing new item {} on container {}", item, container);
-        item.move(container);
-        ((EntityLocal)item).setAttribute(TEST_METRIC, (int)workrate);
-        return item;
-    }
-    
-    protected static MockItemEntity newLockedItem(TestApplication app, MockContainerEntity container, String name, double workrate) {
-        MockItemEntity item = app.createAndManageChild(EntitySpec.create(MockItemEntity.class)
-                .displayName(name)
-                .configure(Movable.IMMOVABLE, true));
-        LOG.debug("Managed new item {} on container {}", item, container);
-        item.move(container);
-        ((EntityLocal)item).setAttribute(TEST_METRIC, (int)workrate);
-        return item;
-    }
-    
-    /**
-     * Asks the item directly for its workrate.
-     */
-    protected static double getItemWorkrate(MockItemEntity item) {
-        Object result = item.getAttribute(TEST_METRIC);
-        return (result == null ? 0 : ((Number) result).doubleValue());
-    }
-    
-    /**
-     * Asks the container for its items, and then each of those items directly for their workrates; returns the total.
-     */
-    protected static double getContainerWorkrate(MockContainerEntity container) {
-        double result = 0.0;
-        Preconditions.checkNotNull(container, "container");
-        for (Movable item : container.getBalanceableItems()) {
-            Preconditions.checkNotNull(item, "item in container");
-            assertEquals(item.getContainerId(), container.getId());
-            result += getItemWorkrate((MockItemEntity)item);
-        }
-        return result;
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/BalanceableWorkerPoolTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/BalanceableWorkerPoolTest.java b/policy/src/test/java/brooklyn/policy/loadbalancing/BalanceableWorkerPoolTest.java
deleted file mode 100644
index e744720..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/BalanceableWorkerPoolTest.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.fail;
-
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.AbstractGroup;
-import brooklyn.entity.basic.AbstractGroupImpl;
-import brooklyn.entity.basic.ApplicationBuilder;
-import brooklyn.entity.basic.DynamicGroup;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.trait.Resizable;
-import org.apache.brooklyn.location.basic.SimulatedLocation;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.exceptions.Exceptions;
-
-import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableList;
-
-public class BalanceableWorkerPoolTest {
-
-    private static final Logger LOG = LoggerFactory.getLogger(BalanceableWorkerPoolTest.class);
-    
-    protected static final long TIMEOUT_MS = 10*1000;
-    protected static final long SHORT_WAIT_MS = 250;
-    
-    protected static final long CONTAINER_STARTUP_DELAY_MS = 100;
-    
-    protected TestApplication app;
-    protected SimulatedLocation loc;
-    protected BalanceableWorkerPool pool;
-    protected Group containerGroup;
-    protected Group itemGroup;
-    
-    @BeforeMethod(alwaysRun=true)
-    public void before() {
-        loc = new SimulatedLocation(MutableMap.of("name", "loc"));
-        
-        app = ApplicationBuilder.newManagedApp(TestApplication.class);
-        containerGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
-                .displayName("containerGroup")
-                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockContainerEntity.class)));
-        itemGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
-                .displayName("itemGroup")
-                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockItemEntity.class)));
-        pool = app.createAndManageChild(EntitySpec.create(BalanceableWorkerPool.class));
-        pool.setContents(containerGroup, itemGroup);
-        
-        app.start(ImmutableList.of(loc));
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void after() {
-        if (app != null) Entities.destroyAll(app.getManagementContext());
-    }
-    
-    @Test
-    public void testDefaultResizeFailsIfContainerGroupNotResizable() throws Exception {
-        try {
-            pool.resize(1);
-            fail();
-        } catch (Exception e) {
-            if (Exceptions.getFirstThrowableOfType(e, UnsupportedOperationException.class) == null) throw e;
-        }
-    }
-    
-    @Test
-    public void testDefaultResizeCallsResizeOnContainerGroup() {
-        LocallyResizableGroup resizable = app.createAndManageChild(EntitySpec.create(LocallyResizableGroup.class));
-        
-        BalanceableWorkerPool pool2 = app.createAndManageChild(EntitySpec.create(BalanceableWorkerPool.class));
-        pool2.setContents(resizable, itemGroup);
-        Entities.manage(pool2);
-        
-        pool2.resize(123);
-        assertEquals(resizable.getCurrentSize(), (Integer) 123);
-    }
-    
-    @Test
-    public void testCustomResizableCalledWhenResizing() {
-        LocallyResizableGroup resizable = app.createAndManageChild(EntitySpec.create(LocallyResizableGroup.class));
-        
-        pool.setResizable(resizable);
-        
-        pool.resize(123);
-        assertEquals(resizable.getCurrentSize(), (Integer)123);
-    }
-
-    @ImplementedBy(LocallyResizableGroupImpl.class)
-    public static interface LocallyResizableGroup extends AbstractGroup, Resizable {
-    }
-    
-    public static class LocallyResizableGroupImpl extends AbstractGroupImpl implements LocallyResizableGroup {
-        private int size = 0;
-
-        @Override
-        public Integer resize(Integer newSize) {
-            size = newSize;
-            return size;
-        }
-        @Override
-        public Integer getCurrentSize() {
-            return size;
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/ItemsInContainersGroupTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/ItemsInContainersGroupTest.java b/policy/src/test/java/brooklyn/policy/loadbalancing/ItemsInContainersGroupTest.java
deleted file mode 100644
index cf47359..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/ItemsInContainersGroupTest.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import static org.testng.Assert.assertEquals;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.ApplicationBuilder;
-import brooklyn.entity.basic.DynamicGroup;
-import brooklyn.entity.basic.Entities;
-import org.apache.brooklyn.location.basic.SimulatedLocation;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-
-import com.google.common.base.Predicate;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-
-public class ItemsInContainersGroupTest {
-
-    // all tests are 20ms or less, but use a big timeout just in case very slow machine!
-    private static final long TIMEOUT_MS = 15000;
-    
-    private TestApplication app;
-    private SimulatedLocation loc;
-    private Group containerGroup;
-    private ItemsInContainersGroup itemGroup;
-
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        loc = new SimulatedLocation(MutableMap.of("name", "loc"));
-        
-        app = ApplicationBuilder.newManagedApp(TestApplication.class);
-        containerGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
-                .displayName("containerGroup")
-                .configure(DynamicGroup.ENTITY_FILTER, new Predicate<Entity>() {
-                    public boolean apply(Entity input) {
-                        return input instanceof MockContainerEntity && 
-                                input.getConfig(MockContainerEntity.MOCK_MEMBERSHIP) == "ingroup";
-                    }}));
-        itemGroup = app.createAndManageChild(EntitySpec.create(ItemsInContainersGroup.class)
-                .displayName("itemGroup"));
-        itemGroup.setContainers(containerGroup);
-        
-        app.start(ImmutableList.of(loc));
-    }
-
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        if (app != null) Entities.destroyAll(app.getManagementContext());
-    }
-
-    @Test
-    public void testSimpleMembership() throws Exception {
-        MockContainerEntity containerIn = newContainer(app, "A", "ingroup");
-        MockItemEntity item1 = newItem(app, containerIn, "1");
-        MockItemEntity item2 = newItem(app, containerIn, "2");
-        
-        assertItemsEventually(item1, item2);
-    }
-
-    @Test
-    public void testFilterIsAppliedToItems() throws Exception {
-        itemGroup.stop();
-        Entities.unmanage(itemGroup);
-        
-        itemGroup = app.createAndManageChild(EntitySpec.create(ItemsInContainersGroup.class)
-                .displayName("itemGroupWithDispName2")
-                .configure(ItemsInContainersGroup.ITEM_FILTER, new Predicate<Entity>() {
-                    public boolean apply(Entity input) {
-                        return "2".equals(input.getDisplayName());
-                    }}));
-        itemGroup.setContainers(containerGroup);
-        
-        MockContainerEntity containerIn = newContainer(app, "A", "ingroup");
-        MockItemEntity item1 = newItem(app, containerIn, "1");
-        MockItemEntity item2 = newItem(app, containerIn, "2");
-        
-        assertItemsEventually(item2); // does not include item1
-    }
-
-    @Test
-    public void testItemsInOtherContainersIgnored() throws Exception {
-        MockContainerEntity containerOut = newContainer(app, "A", "outgroup");
-        MockItemEntity item1 = newItem(app, containerOut, "1");
-        
-        assertItemsEventually();
-    }
-    
-    @Test
-    public void testItemMovedInIsAdded() throws Exception {
-        MockContainerEntity containerIn = newContainer(app, "A", "ingroup");
-        MockContainerEntity containerOut = newContainer(app, "A", "outgroup");
-        MockItemEntity item1 = newItem(app, containerOut, "1");
-        item1.move(containerIn);
-        
-        assertItemsEventually(item1);
-    }
-
-    @Test
-    public void testItemMovedOutIsRemoved() throws Exception {
-        MockContainerEntity containerIn = newContainer(app, "A", "ingroup");
-        MockContainerEntity containerOut = newContainer(app, "A", "outgroup");
-        MockItemEntity item1 = newItem(app, containerIn, "1");
-        assertItemsEventually(item1);
-        
-        item1.move(containerOut);
-        assertItemsEventually();
-    }
-
-    /*
-     * Previously could fail if...
-     * ItemsInContainersGroupImpl listener got notified of Movable.CONTAINER after entity was unmanaged
-     * (because being done in concurrent threads).
-     * This called ItemsInContainersGroupImpl.onItemMoved, which called addMember to add it back in again.
-     * In AbstractGroup.addMember, we now check if the entity is still managed, to 
-     * ensure there is synchronization for concurrent calls to add/remove member.
-     */
-    @Test
-    public void testItemUnmanagedIsRemoved() throws Exception {
-        MockContainerEntity containerIn = newContainer(app, "A", "ingroup");
-        MockItemEntity item1 = newItem(app, containerIn, "1");
-        assertItemsEventually(item1);
-        
-        Entities.unmanage(item1);
-        assertItemsEventually();
-    }
-
-    // TODO How to test this? Will it be used?
-    // Adding a new container then adding items to it is tested in many other methods.
-    @Test(enabled=false)
-    public void testContainerAddedWillAddItsItems() throws Exception {
-    }
-
-    @Test
-    public void testContainerRemovedWillRemoveItsItems() throws Exception {
-        MockContainerEntity containerA = newContainer(app, "A", "ingroup");
-        MockItemEntity item1 = newItem(app, containerA, "1");
-        assertItemsEventually(item1);
-        
-        Entities.unmanage(containerA);
-        assertItemsEventually();
-    }
-
-    private void assertItemsEventually(final MockItemEntity... expected) {
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            public void run() {
-                assertEquals(ImmutableSet.copyOf(itemGroup.getMembers()), ImmutableSet.copyOf(expected));
-            }});
-    }   
-     
-    private MockContainerEntity newContainer(TestApplication app, String name, String membership) {
-        MockContainerEntity container = app.createAndManageChild(EntitySpec.create(MockContainerEntity.class)
-                        .displayName(name)
-                        .configure(MockContainerEntity.MOCK_MEMBERSHIP, membership));
-        container.start(ImmutableList.of(loc));
-        return container;
-    }
-    
-    private static MockItemEntity newItem(TestApplication app, MockContainerEntity container, String name) {
-        MockItemEntity item = app.createAndManageChild(EntitySpec.create(MockItemEntity.class)
-                .displayName(name));
-        item.move(container);
-        return item;
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingModelTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingModelTest.java b/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingModelTest.java
deleted file mode 100644
index cae86e7..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingModelTest.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import static org.testng.Assert.assertEquals;
-
-import java.util.Collections;
-
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-
-public class LoadBalancingModelTest {
-
-    private static final double PRECISION = 0.00001;
-    
-    private MockContainerEntity container1 = new MockContainerEntityImpl();
-    private MockContainerEntity container2 = new MockContainerEntityImpl();
-    private MockItemEntity item1 = new MockItemEntityImpl();
-    private MockItemEntity item2 = new MockItemEntityImpl();
-    private MockItemEntity item3 = new MockItemEntityImpl();
-    
-    private DefaultBalanceablePoolModel<MockContainerEntity, MockItemEntity> model;
-
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        model = new DefaultBalanceablePoolModel<MockContainerEntity, MockItemEntity>("myname");
-    }
-
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        // nothing to tear down; no management context created
-    }
-
-    @Test
-    public void testPoolRatesCorrectlySumContainers() throws Exception {
-        model.onContainerAdded(container1, 10d, 20d);
-        model.onContainerAdded(container2, 11d, 22d);
-        
-        assertEquals(model.getPoolLowThreshold(), 10d+11d, PRECISION);
-        assertEquals(model.getPoolHighThreshold(), 20d+22d, PRECISION);
-    }
-    
-    @Test
-    public void testPoolRatesCorrectlySumItems() throws Exception {
-        model.onContainerAdded(container1, 10d, 20d);
-        model.onItemAdded(item1, container1, true);
-        model.onItemAdded(item2, container1, true);
-        
-        model.onItemWorkrateUpdated(item1, 1d);
-        assertEquals(model.getCurrentPoolWorkrate(), 1d, PRECISION);
-        
-        model.onItemWorkrateUpdated(item2, 2d);
-        assertEquals(model.getCurrentPoolWorkrate(), 1d+2d, PRECISION);
-        
-        model.onItemWorkrateUpdated(item2, 4d);
-        assertEquals(model.getCurrentPoolWorkrate(), 1d+4d, PRECISION);
-        
-        model.onItemRemoved(item1);
-        assertEquals(model.getCurrentPoolWorkrate(), 4d, PRECISION);
-    }
-    
-    @Test
-    public void testWorkrateUpdateAfterItemRemovalIsNotRecorded() throws Exception {
-        model.onContainerAdded(container1, 10d, 20d);
-        model.onItemAdded(item1, container1, true);
-        model.onItemRemoved(item1);
-        model.onItemWorkrateUpdated(item1, 123d);
-        
-        assertEquals(model.getCurrentPoolWorkrate(), 0d, PRECISION);
-        assertEquals(model.getContainerWorkrates().get(container1), 0d, PRECISION);
-        assertEquals(model.getItemWorkrate(item1), null);
-    }
-    
-    @Test
-    public void testItemMovedWillUpdateContainerWorkrates() throws Exception {
-        model.onContainerAdded(container1, 10d, 20d);
-        model.onContainerAdded(container2, 11d, 21d);
-        model.onItemAdded(item1, container1, false);
-        model.onItemWorkrateUpdated(item1, 123d);
-        
-        model.onItemMoved(item1, container2);
-        
-        assertEquals(model.getItemsForContainer(container1), Collections.emptySet());
-        assertEquals(model.getItemsForContainer(container2), ImmutableSet.of(item1));
-        assertEquals(model.getItemWorkrate(item1), 123d);
-        assertEquals(model.getTotalWorkrate(container1), 0d);
-        assertEquals(model.getTotalWorkrate(container2), 123d);
-        assertEquals(model.getItemWorkrates(container1), Collections.emptyMap());
-        assertEquals(model.getItemWorkrates(container2), ImmutableMap.of(item1, 123d));
-        assertEquals(model.getContainerWorkrates(), ImmutableMap.of(container1, 0d, container2, 123d));
-        assertEquals(model.getCurrentPoolWorkrate(), 123d);
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicyConcurrencyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicyConcurrencyTest.java b/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicyConcurrencyTest.java
deleted file mode 100644
index 69d380b..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicyConcurrencyTest.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Queue;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-
-import com.google.common.collect.Lists;
-
-public class LoadBalancingPolicyConcurrencyTest extends AbstractLoadBalancingPolicyTest {
-    
-    private static final Logger LOG = LoggerFactory.getLogger(LoadBalancingPolicyConcurrencyTest.class);
-
-    private static final double WORKRATE_JITTER = 2d;
-    private static final int NUM_CONTAINERS = 20;
-    private static final int WORKRATE_UPDATE_PERIOD_MS = 1000;
-    
-    private ScheduledExecutorService scheduledExecutor;
-
-    @BeforeMethod(alwaysRun=true)
-    @Override
-    public void before() {
-        scheduledExecutor = Executors.newScheduledThreadPool(10);
-        super.before();
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    @Override
-    public void after() {
-        if (scheduledExecutor != null) scheduledExecutor.shutdownNow();
-        super.after();
-    }
-    
-    @Test
-    public void testSimplePeriodicWorkrateUpdates() {
-        List<MockItemEntity> items = Lists.newArrayList();
-        List<MockContainerEntity> containers = Lists.newArrayList();
-        
-        for (int i = 0; i < NUM_CONTAINERS; i++) {
-            containers.add(newContainer(app, "container"+i, 10, 30));
-        }
-        for (int i = 0; i < NUM_CONTAINERS; i++) {
-            newItemWithPeriodicWorkrates(app, containers.get(0), "item"+i, 20);
-        }
-
-        assertWorkratesEventually(containers, items, Collections.nCopies(NUM_CONTAINERS, 20d), WORKRATE_JITTER);
-    }
-    
-    @Test
-    public void testConcurrentlyAddContainers() {
-        final Queue<MockContainerEntity> containers = new ConcurrentLinkedQueue<MockContainerEntity>();
-        final List<MockItemEntity> items = Lists.newArrayList();
-        
-        containers.add(newContainer(app, "container-orig", 10, 30));
-        
-        for (int i = 0; i < NUM_CONTAINERS; i++) {
-            items.add(newItemWithPeriodicWorkrates(app, containers.iterator().next(), "item"+i, 20));
-        }
-        for (int i = 0; i < NUM_CONTAINERS-1; i++) {
-            final int index = i;
-            scheduledExecutor.submit(new Callable<Void>() {
-                @Override public Void call() {
-                    containers.add(newContainer(app, "container"+index, 10, 30));
-                    return null;
-                }});
-        }
-
-        assertWorkratesEventually(containers, items, Collections.nCopies(NUM_CONTAINERS, 20d), WORKRATE_JITTER);
-    }
-    
-    @Test
-    public void testConcurrentlyAddItems() {
-        final Queue<MockItemEntity> items = new ConcurrentLinkedQueue<MockItemEntity>();
-        final List<MockContainerEntity> containers = Lists.newArrayList();
-        
-        for (int i = 0; i < NUM_CONTAINERS; i++) {
-            containers.add(newContainer(app, "container"+i, 10, 30));
-        }
-        for (int i = 0; i < NUM_CONTAINERS; i++) {
-            final int index = i;
-            scheduledExecutor.submit(new Callable<Void>() {
-                @Override public Void call() {
-                    items.add(newItemWithPeriodicWorkrates(app, containers.get(0), "item"+index, 20));
-                    return null;
-                }});
-        }
-        assertWorkratesEventually(containers, items, Collections.nCopies(NUM_CONTAINERS, 20d), WORKRATE_JITTER);
-    }
-    
-    // TODO Got IndexOutOfBoundsException from containers.last()
-    @Test(groups="WIP", invocationCount=100)
-    public void testConcurrentlyRemoveContainers() {
-        List<MockItemEntity> items = Lists.newArrayList();
-        final List<MockContainerEntity> containers = Lists.newArrayList();
-        
-        for (int i = 0; i < NUM_CONTAINERS; i++) {
-            containers.add(newContainer(app, "container"+i, 15, 45));
-        }
-        for (int i = 0; i < NUM_CONTAINERS; i++) {
-            items.add(newItemWithPeriodicWorkrates(app, containers.get(i), "item"+i, 20));
-        }
-        
-        final List<MockContainerEntity> containersToStop = Lists.newArrayList();
-        for (int i = 0; i < NUM_CONTAINERS/2; i++) {
-            containersToStop.add(containers.remove(0));
-        }
-        for (final MockContainerEntity containerToStop : containersToStop) {
-            scheduledExecutor.submit(new Callable<Void>() {
-                @Override public Void call() {
-                    try {
-                        containerToStop.offloadAndStop(containers.get(containers.size()-1));
-                        Entities.unmanage(containerToStop);
-                    } catch (Throwable t) {
-                        LOG.error("Error stopping container "+containerToStop, t);
-                    }
-                    return null;
-                }});
-        }
-        
-        assertWorkratesEventually(containers, items, Collections.nCopies((int)(NUM_CONTAINERS/2), 40d), WORKRATE_JITTER*2);
-    }
-    
-    @Test(groups="WIP")
-    public void testConcurrentlyRemoveItems() {
-        List<MockItemEntity> items = Lists.newArrayList();
-        List<MockContainerEntity> containers = Lists.newArrayList();
-        
-        for (int i = 0; i < NUM_CONTAINERS; i++) {
-            containers.add(newContainer(app, "container"+i, 15, 45));
-        }
-        for (int i = 0; i < NUM_CONTAINERS*2; i++) {
-            items.add(newItemWithPeriodicWorkrates(app, containers.get(i%NUM_CONTAINERS), "item"+i, 20));
-        }
-        // should now have item0 and item{0+NUM_CONTAINERS} on container0, etc
-        
-        for (int i = 0; i < NUM_CONTAINERS; i++) {
-            // not removing consecutive items as that would leave it balanced!
-            int indexToStop = (i < NUM_CONTAINERS/2) ? NUM_CONTAINERS : 0; 
-            final MockItemEntity itemToStop = items.remove(indexToStop);
-            scheduledExecutor.submit(new Callable<Void>() {
-                @Override public Void call() {
-                    try {
-                        itemToStop.stop();
-                        Entities.unmanage(itemToStop);
-                    } catch (Throwable t) {
-                        LOG.error("Error stopping item "+itemToStop, t);
-                    }
-                    return null;
-                }});
-        }
-        
-        assertWorkratesEventually(containers, items, Collections.nCopies(NUM_CONTAINERS, 20d), WORKRATE_JITTER);
-    }
-    
-    protected MockItemEntity newItemWithPeriodicWorkrates(TestApplication app, MockContainerEntity container, String name, double workrate) {
-        MockItemEntity item = newItem(app, container, name, workrate);
-        scheduleItemWorkrateUpdates(item, workrate, WORKRATE_JITTER);
-        return item;
-    }
-    
-    private void scheduleItemWorkrateUpdates(final MockItemEntity item, final double workrate, final double jitter) {
-        final AtomicReference<Future<?>> futureRef = new AtomicReference<Future<?>>();
-        Future<?> future = scheduledExecutor.scheduleAtFixedRate(
-                new Runnable() {
-                    @Override public void run() {
-                        if (item.isStopped() && futureRef.get() != null) {
-                            futureRef.get().cancel(true);
-                            return;
-                        }
-                        double jitteredWorkrate = workrate + (random.nextDouble()*jitter*2 - jitter);
-                        ((EntityLocal)item).setAttribute(TEST_METRIC, (int) Math.max(0, jitteredWorkrate));
-                    }
-                },
-                0, WORKRATE_UPDATE_PERIOD_MS, TimeUnit.MILLISECONDS);
-        futureRef.set(future);
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicySoakTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicySoakTest.java b/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicySoakTest.java
deleted file mode 100644
index 1bb8dc4..0000000
--- a/policy/src/test/java/brooklyn/policy/loadbalancing/LoadBalancingPolicySoakTest.java
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-
-import com.google.common.base.Function;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-
-public class LoadBalancingPolicySoakTest extends AbstractLoadBalancingPolicyTest {
-
-    private static final Logger LOG = LoggerFactory.getLogger(LoadBalancingPolicySoakTest.class);
-    
-    private static final long TIMEOUT_MS = 40*1000;
-    
-    @Test
-    public void testLoadBalancingQuickTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 1;
-        config.numContainers = 5;
-        config.numItems = 5;
-        config.lowThreshold = 200;
-        config.highThreshold = 300;
-        config.totalRate = (int) (config.numContainers*(0.95*config.highThreshold));
-    
-        runLoadBalancingSoakTest(config);
-    }
-    
-    @Test
-    public void testLoadBalancingManyItemsQuickTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 1;
-        config.numContainers = 5;
-        config.numItems = 30;
-        config.lowThreshold = 200;
-        config.highThreshold = 300;
-        config.numContainerStopsPerCycle = 1;
-        config.numItemStopsPerCycle = 1;
-        config.totalRate = (int) (config.numContainers*(0.95*config.highThreshold));
-    
-        runLoadBalancingSoakTest(config);
-    }
-    
-    @Test(groups={"Integration","Acceptance"}) // acceptance group, because it's slow to run many cycles
-    public void testLoadBalancingSoakTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 100;
-        config.numContainers = 5;
-        config.numItems = 5;
-        config.lowThreshold = 200;
-        config.highThreshold = 300;
-        config.totalRate = (int) (config.numContainers*(0.95*config.highThreshold));
-    
-        runLoadBalancingSoakTest(config);
-    }
-
-    @Test(groups={"Integration","Acceptance"}) // acceptance group, because it's slow to run many cycles
-    public void testLoadBalancingManyItemsSoakTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 100;
-        config.numContainers = 5;
-        config.numItems = 30;
-        config.lowThreshold = 200;
-        config.highThreshold = 300;
-        config.totalRate = (int) (config.numContainers*(0.95*config.highThreshold));
-        config.numContainerStopsPerCycle = 3;
-        config.numItemStopsPerCycle = 10;
-        
-        runLoadBalancingSoakTest(config);
-    }
-
-    @Test(groups={"Integration","Acceptance"})
-    public void testLoadBalancingManyManyItemsTest() {
-        RunConfig config = new RunConfig();
-        config.numCycles = 1;
-        config.numContainers = 5;
-        config.numItems = 1000;
-        config.lowThreshold = 2000;
-        config.highThreshold = 3000;
-        config.numContainerStopsPerCycle = 0;
-        config.numItemStopsPerCycle = 0;
-        config.totalRate = (int) (config.numContainers*(0.95*config.highThreshold));
-        config.verbose = false;
-        
-        runLoadBalancingSoakTest(config);
-    }
-    
-    private void runLoadBalancingSoakTest(RunConfig config) {
-        final int numCycles = config.numCycles;
-        final int numContainers = config.numContainers;
-        final int numItems = config.numItems;
-        final double lowThreshold = config.lowThreshold;
-        final double highThreshold = config.highThreshold;
-        final int totalRate = config.totalRate;
-        final int numContainerStopsPerCycle = config.numContainerStopsPerCycle;
-        final int numItemStopsPerCycle = config.numItemStopsPerCycle;
-        final boolean verbose = config.verbose;
-        
-        MockItemEntityImpl.totalMoveCount.set(0);
-        
-        final List<MockContainerEntity> containers = new ArrayList<MockContainerEntity>();
-        final List<MockItemEntity> items = new ArrayList<MockItemEntity>();
-
-        for (int i = 1; i <= numContainers; i++) {
-            MockContainerEntity container = newContainer(app, "container-"+i, lowThreshold, highThreshold);
-            containers.add(container);
-        }
-        for (int i = 1; i <= numItems; i++) {
-            MockItemEntity item = newItem(app, containers.get(0), "item-"+i, 5);
-            items.add(item);
-        }
-        
-        for (int i = 1; i <= numCycles; i++) {
-            LOG.info(LoadBalancingPolicySoakTest.class.getSimpleName()+": cycle "+i);
-            
-            // Stop items, and start others
-            for (int j = 1; j <= numItemStopsPerCycle; j++) {
-                int itemIndex = random.nextInt(numItems);
-                MockItemEntity itemToStop = items.get(itemIndex);
-                itemToStop.stop();
-                LOG.debug("Unmanaging item {}", itemToStop);
-                Entities.unmanage(itemToStop);
-                items.set(itemIndex, newItem(app, containers.get(0), "item-"+(itemIndex+1)+"."+i+"."+j, 5));
-            }
-            
-            // Repartition the load across the items
-            final List<Integer> itemRates = randomlyDivideLoad(numItems, totalRate, 0, (int)highThreshold);
-            
-            for (int j = 0; j < numItems; j++) {
-                MockItemEntity item = items.get(j);
-                ((EntityLocal)item).setAttribute(MockItemEntity.TEST_METRIC, itemRates.get(j));
-            }
-                
-            // Stop containers, and start others
-            for (int j = 1; j <= numContainerStopsPerCycle; j++) {
-                int containerIndex = random.nextInt(numContainers);
-                MockContainerEntity containerToStop = containers.get(containerIndex);
-                containerToStop.offloadAndStop(containers.get((containerIndex+1)%numContainers));
-                LOG.debug("Unmanaging container {}", containerToStop);
-                Entities.unmanage(containerToStop);
-                
-                MockContainerEntity containerToAdd = newContainer(app, "container-"+(containerIndex+1)+"."+i+"."+j, lowThreshold, highThreshold);
-                containers.set(containerIndex, containerToAdd);
-            }
-
-            // Assert that the items become balanced again
-            Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-                @Override public void run() {
-                    Iterable<Double> containerRates = Iterables.transform(containers, new Function<MockContainerEntity, Double>() {
-                        @Override public Double apply(MockContainerEntity input) {
-                            return (double) input.getWorkrate();
-                        }});
-                    
-                    String errMsg;
-                    if (verbose) {
-                        errMsg = verboseDumpToString(containers, items)+"; itemRates="+itemRates;
-                    } else {
-                        errMsg = containerRates+"; totalMoves="+MockItemEntityImpl.totalMoveCount;
-                    }
-                    
-                    // Check that haven't lost any items
-                    // (as observed in one jenkins build failure: 2014-03-18; but that could also be 
-                    // explained by errMsg generated in the middle of a move)
-                    List<Entity> itemsFromModel = Lists.newArrayList();
-                    List<Entity> itemsFromContainers = Lists.newArrayList();
-                    for (Entity container : model.getPoolContents()) {
-                        itemsFromModel.addAll(model.getItemsForContainer(container));
-                    }
-                    for (MockContainerEntity container : containers) {
-                        itemsFromContainers.addAll(container.getBalanceableItems());
-                    }
-                    Asserts.assertEqualsIgnoringOrder(itemsFromModel, items, true, errMsg);
-                    Asserts.assertEqualsIgnoringOrder(itemsFromContainers, items, true, errMsg);
-                    
-                    // Check overall container rates are balanced
-                    assertEquals(sum(containerRates), sum(itemRates), errMsg);
-                    for (double containerRate : containerRates) {
-                        assertTrue(containerRate >= lowThreshold, errMsg);
-                        assertTrue(containerRate <= highThreshold, errMsg);
-                    }
-                }});
-        }
-    }
-    
-    private static class RunConfig {
-        int numCycles = 1;
-        int numContainers = 5;
-        int numItems = 5;
-        double lowThreshold = 200;
-        double highThreshold = 300;
-        int totalRate = (int) (5*(0.95*highThreshold));
-        int numContainerStopsPerCycle = 1;
-        int numItemStopsPerCycle = 1;
-        boolean verbose = true;
-    }
-
-    // Testing conveniences.
-    
-    private double sum(Iterable<? extends Number> vals) {
-        double total = 0;;
-        for (Number val : vals) {
-            total += val.doubleValue();
-        }
-        return total;
-    }
-    
-    /**
-     * Distributes a given load across a number of items randomly. The variability in load for an item is dictated by the variance,
-     * but the total will always equal totalLoad.
-     * 
-     * The distribution of load is skewed: one side of the list will have bigger values than the other.
-     * Which side is skewed will vary, so when balancing a policy will find that things have entirely changed.
-     * 
-     * TODO This is not particularly good at distributing load, but it's random and skewed enough to force rebalancing.
-     */
-    private List<Integer> randomlyDivideLoad(int numItems, int totalLoad, int min, int max) {
-        List<Integer> result = new ArrayList<Integer>(numItems);
-        int totalRemaining = totalLoad;
-        int variance = 3;
-        int skew = 3;
-        
-        for (int i = 0; i < numItems; i++) {
-            int itemsRemaining = numItems-i;
-            int itemFairShare = (totalRemaining/itemsRemaining);
-            double skewFactor = ((double)i/numItems)*2 - 1; // a number between -1 and 1, depending how far through the item set we are
-            int itemSkew = (int) (random.nextInt(skew)*skewFactor);
-            int itemLoad = itemFairShare + (random.nextInt(variance*2)-variance) + itemSkew;
-            itemLoad = Math.max(min, itemLoad);
-            itemLoad = Math.min(totalRemaining, itemLoad);
-            itemLoad = Math.min(max, itemLoad);
-            result.add(itemLoad);
-            totalRemaining -= itemLoad;
-        }
-
-        if (random.nextBoolean()) Collections.reverse(result);
-        
-        assertTrue(sum(result) <= totalLoad, "totalLoad="+totalLoad+"; result="+result);
-        
-        return result;
-    }
-}


[18/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/ha/ConnectionFailureDetector.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/ha/ConnectionFailureDetector.java b/policy/src/main/java/brooklyn/policy/ha/ConnectionFailureDetector.java
deleted file mode 100644
index ff5d60e..0000000
--- a/policy/src/main/java/brooklyn/policy/ha/ConnectionFailureDetector.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import org.apache.brooklyn.api.catalog.Catalog;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.event.basic.BasicConfigKey;
-import brooklyn.event.basic.BasicNotificationSensor;
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.util.guava.Maybe;
-import brooklyn.util.net.Networking;
-import brooklyn.util.time.Duration;
-
-import com.google.common.net.HostAndPort;
-
-/**
- * Monitors a given {@link HostAndPort}, to emit HASensors.CONNECTION_FAILED and HASensors.CONNECTION_RECOVERED 
- * if the connection is lost/restored.
- */
-@Catalog(name="Connection Failure Detector", description="HA policy for monitoring a host:port, "
-        + "emitting an event if the connection is lost/restored")
-public class ConnectionFailureDetector extends AbstractFailureDetector {
-
-    public static final ConfigKey<HostAndPort> ENDPOINT = ConfigKeys.newConfigKey(HostAndPort.class, "connectionFailureDetector.endpoint");
-
-    public static final ConfigKey<Duration> POLL_PERIOD = ConfigKeys.newConfigKey(Duration.class, "connectionFailureDetector.pollPeriod", "", Duration.ONE_SECOND);
-
-    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_FAILED = HASensors.CONNECTION_FAILED;
-
-    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_RECOVERED = HASensors.CONNECTION_RECOVERED;
-
-    @SetFromFlag("connectionFailedStabilizationDelay")
-    public static final ConfigKey<Duration> CONNECTION_FAILED_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
-            .name("connectionFailureDetector.serviceFailedStabilizationDelay")
-            .description("Time period for which the connection must be consistently down for "
-                    + "(e.g. doesn't report down-up-down) before concluding failure. "
-                    + "Note that long TCP timeouts mean there can be long (e.g. 70 second) "
-                    + "delays in noticing a connection refused condition.")
-            .defaultValue(Duration.ZERO)
-            .build();
-
-    @SetFromFlag("connectionRecoveredStabilizationDelay")
-    public static final ConfigKey<Duration> CONNECTION_RECOVERED_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
-            .name("connectionFailureDetector.serviceRecoveredStabilizationDelay")
-            .description("For a failed connection, time period for which the connection must be consistently up for (e.g. doesn't report up-down-up) before concluding recovered")
-            .defaultValue(Duration.ZERO)
-            .build();
-
-    @Override
-    public void init() {
-        super.init();
-        getRequiredConfig(ENDPOINT); // just to confirm it's set, failing fast
-        if (config().getRaw(SENSOR_FAILED).isAbsent()) {
-            config().set(SENSOR_FAILED, CONNECTION_FAILED);
-        }
-        if (config().getRaw(SENSOR_RECOVERED).isAbsent()) {
-            config().set(SENSOR_RECOVERED, CONNECTION_RECOVERED);
-        }
-    }
-
-    @Override
-    protected CalculatedStatus calculateStatus() {
-        HostAndPort endpoint = getConfig(ENDPOINT);
-        boolean isHealthy = Networking.isReachable(endpoint);
-        return new BasicCalculatedStatus(isHealthy, "endpoint=" + endpoint);
-    }
-
-    //Persistence compatibility overrides
-    @Override
-    protected Duration getPollPeriod() {
-        return getConfig(POLL_PERIOD);
-    }
-
-    @Override
-    protected Duration getFailedStabilizationDelay() {
-        return getConfig(CONNECTION_FAILED_STABILIZATION_DELAY);
-    }
-
-    @Override
-    protected Duration getRecoveredStabilizationDelay() {
-        return getConfig(CONNECTION_RECOVERED_STABILIZATION_DELAY);
-    }
-
-    @SuppressWarnings("unchecked")
-    @Override
-    protected Sensor<FailureDescriptor> getSensorFailed() {
-        Maybe<Object> sensorFailed = config().getRaw(SENSOR_FAILED);
-        if (sensorFailed.isPresent()) {
-            return (Sensor<FailureDescriptor>)sensorFailed.get();
-        } else {
-            return CONNECTION_FAILED;
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    @Override
-    protected Sensor<FailureDescriptor> getSensorRecovered() {
-        Maybe<Object> sensorRecovered = config().getRaw(SENSOR_RECOVERED);
-        if (sensorRecovered.isPresent()) {
-            return (Sensor<FailureDescriptor>)sensorRecovered.get();
-        } else {
-            return CONNECTION_RECOVERED;
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/ha/HASensors.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/ha/HASensors.java b/policy/src/main/java/brooklyn/policy/ha/HASensors.java
deleted file mode 100644
index b940aa0..0000000
--- a/policy/src/main/java/brooklyn/policy/ha/HASensors.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import brooklyn.event.basic.BasicNotificationSensor;
-
-import com.google.common.base.Objects;
-
-public class HASensors {
-
-    public static final BasicNotificationSensor<FailureDescriptor> ENTITY_FAILED = new BasicNotificationSensor<FailureDescriptor>(
-            FailureDescriptor.class, "ha.entityFailed", "Indicates that an entity has failed");
-    
-    public static final BasicNotificationSensor<FailureDescriptor> ENTITY_RECOVERED = new BasicNotificationSensor<FailureDescriptor>(
-            FailureDescriptor.class, "ha.entityRecovered", "Indicates that a previously failed entity has recovered");
-    
-    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_FAILED = new BasicNotificationSensor<FailureDescriptor>(
-            FailureDescriptor.class, "ha.connectionFailed", "Indicates that a connection has failed");
-    
-    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_RECOVERED = new BasicNotificationSensor<FailureDescriptor>(
-            FailureDescriptor.class, "ha.connectionRecovered", "Indicates that a previously failed connection has recovered");
-    
-    // TODO How to make this serializable with the entity reference
-    public static class FailureDescriptor {
-        private final Object component;
-        private final String description;
-        
-        public FailureDescriptor(Object component, String description) {
-            this.component = component;
-            this.description = description;
-        }
-        
-        public Object getComponent() {
-            return component;
-        }
-        
-        public String getDescription() {
-            return description;
-        }
-        
-        @Override
-        public String toString() {
-            return Objects.toStringHelper(this).add("component", component).add("description", description).toString();
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/ha/ServiceFailureDetector.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/ha/ServiceFailureDetector.java b/policy/src/main/java/brooklyn/policy/ha/ServiceFailureDetector.java
deleted file mode 100644
index 2e4b719..0000000
--- a/policy/src/main/java/brooklyn/policy/ha/ServiceFailureDetector.java
+++ /dev/null
@@ -1,340 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.core.util.config.ConfigBag;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-import org.apache.brooklyn.core.util.task.BasicTask;
-import org.apache.brooklyn.core.util.task.ScheduledTask;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.Attributes;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.entity.basic.EntityInternal;
-import brooklyn.entity.basic.Lifecycle;
-import brooklyn.entity.basic.ServiceStateLogic;
-import brooklyn.entity.basic.ServiceStateLogic.ComputeServiceState;
-import brooklyn.event.basic.BasicConfigKey;
-import brooklyn.event.basic.BasicNotificationSensor;
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.exceptions.Exceptions;
-import brooklyn.util.time.Duration;
-import brooklyn.util.time.Time;
-
-/** 
- * Emits {@link HASensors#ENTITY_FAILED} whenever the parent's default logic ({@link ComputeServiceState}) would detect a problem,
- * and similarly {@link HASensors#ENTITY_RECOVERED} when recovered.
- * <p>
- * gives more control over suppressing {@link Lifecycle#ON_FIRE}, 
- * for some period of time
- * (or until another process manually sets {@link Attributes#SERVICE_STATE_ACTUAL} to {@value Lifecycle#ON_FIRE},
- * which this enricher will not clear until all problems have gone away)
- */
-//@Catalog(name="Service Failure Detector", description="HA policy for deteting failure of a service")
-public class ServiceFailureDetector extends ServiceStateLogic.ComputeServiceState {
-
-    // TODO Remove duplication between this and MemberFailureDetectionPolicy.
-    // The latter could be re-written to use this. Or could even be deprecated
-    // in favour of this.
-
-    public enum LastPublished {
-        NONE,
-        FAILED,
-        RECOVERED;
-    }
-
-    private static final Logger LOG = LoggerFactory.getLogger(ServiceFailureDetector.class);
-
-    private static final long MIN_PERIOD_BETWEEN_EXECS_MILLIS = 100;
-
-    public static final BasicNotificationSensor<FailureDescriptor> ENTITY_FAILED = HASensors.ENTITY_FAILED;
-
-    @SetFromFlag("onlyReportIfPreviouslyUp")
-    public static final ConfigKey<Boolean> ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP = ConfigKeys.newBooleanConfigKey("onlyReportIfPreviouslyUp", 
-        "Prevents the policy from emitting ENTITY_FAILED if the entity fails on startup (ie has never been up)", true);
-    
-    public static final ConfigKey<Boolean> MONITOR_SERVICE_PROBLEMS = ConfigKeys.newBooleanConfigKey("monitorServiceProblems", 
-        "Whether to monitor service problems, and emit on failures there (if set to false, this monitors only service up)", true);
-
-    @SetFromFlag("serviceOnFireStabilizationDelay")
-    public static final ConfigKey<Duration> SERVICE_ON_FIRE_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
-            .name("serviceOnFire.stabilizationDelay")
-            .description("Time period for which the service must be consistently down for (e.g. doesn't report down-up-down) before concluding ON_FIRE")
-            .defaultValue(Duration.ZERO)
-            .build();
-
-    @SetFromFlag("entityFailedStabilizationDelay")
-    public static final ConfigKey<Duration> ENTITY_FAILED_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
-            .name("entityFailed.stabilizationDelay")
-            .description("Time period for which the service must be consistently down for (e.g. doesn't report down-up-down) before emitting ENTITY_FAILED")
-            .defaultValue(Duration.ZERO)
-            .build();
-
-    @SetFromFlag("entityRecoveredStabilizationDelay")
-    public static final ConfigKey<Duration> ENTITY_RECOVERED_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
-            .name("entityRecovered.stabilizationDelay")
-            .description("For a failed entity, time period for which the service must be consistently up for (e.g. doesn't report up-down-up) before emitting ENTITY_RECOVERED")
-            .defaultValue(Duration.ZERO)
-            .build();
-
-    @SetFromFlag("entityFailedRepublishTime")
-    public static final ConfigKey<Duration> ENTITY_FAILED_REPUBLISH_TIME = BasicConfigKey.builder(Duration.class)
-            .name("entityFailed.republishTime")
-            .description("Publish failed state periodically at the specified intervals, null to disable.")
-            .build();
-
-    protected Long firstUpTime;
-    
-    protected Long currentFailureStartTime = null;
-    protected Long currentRecoveryStartTime = null;
-    
-    protected Long publishEntityFailedTime = null;
-    protected Long publishEntityRecoveredTime = null;
-    protected Long setEntityOnFireTime = null;
-    
-    protected LastPublished lastPublished = LastPublished.NONE;
-
-    private final AtomicBoolean executorQueued = new AtomicBoolean(false);
-    private volatile long executorTime = 0;
-
-    /**
-     * TODO Really don't want this mutex!
-     * ServiceStateLogic.setExpectedState() will call into `onEvent(null)`, so could get concurrent calls.
-     * How to handle that? I don't think `ServiceStateLogic.setExpectedState` should be making the call, but
-     * presumably that is their to remove a race condition so it is set before method returns. Caller shouldn't
-     * rely on that though.
-     * e.g. see `ServiceFailureDetectorTest.testNotifiedOfFailureOnStateOnFire`, where we get two notifications.
-     */
-    private final Object mutex = new Object();
-    
-    public ServiceFailureDetector() {
-        this(new ConfigBag());
-    }
-    
-    public ServiceFailureDetector(Map<String,?> flags) {
-        this(new ConfigBag().putAll(flags));
-    }
-    
-    public ServiceFailureDetector(ConfigBag configBag) {
-        // TODO hierarchy should use ConfigBag, and not change flags
-        super(configBag.getAllConfigMutable());
-    }
-    
-    @Override
-    public void onEvent(SensorEvent<Object> event) {
-        if (firstUpTime==null) {
-            if (event!=null && Attributes.SERVICE_UP.equals(event.getSensor()) && Boolean.TRUE.equals(event.getValue())) {
-                firstUpTime = event.getTimestamp();
-            } else if (event == null && Boolean.TRUE.equals(entity.getAttribute(Attributes.SERVICE_UP))) {
-                // If this enricher is registered after the entity is up, then we'll get a "synthetic" onEvent(null) 
-                firstUpTime = System.currentTimeMillis();
-            }
-        }
-        
-        super.onEvent(event);
-    }
-    
-    @Override
-    protected void setActualState(Lifecycle state) {
-        long now = System.currentTimeMillis();
-
-        synchronized (mutex) {
-            if (state==Lifecycle.ON_FIRE) {
-                if (lastPublished == LastPublished.FAILED) {
-                    if (currentRecoveryStartTime != null) {
-                        if (LOG.isDebugEnabled()) LOG.debug("{} health-check for {}, component was recovering, now failing: {}", new Object[] {this, entity, getExplanation(state)});
-                        currentRecoveryStartTime = null;
-                        publishEntityRecoveredTime = null;
-                    } else {
-                        if (LOG.isTraceEnabled()) LOG.trace("{} health-check for {}, component still failed: {}", new Object[] {this, entity, getExplanation(state)});
-                    }
-                } else {
-                    if (firstUpTime == null && getConfig(ENTITY_FAILED_ONLY_IF_PREVIOUSLY_UP)) {
-                        // suppress; won't publish
-                    } else if (currentFailureStartTime == null) {
-                        if (LOG.isDebugEnabled()) LOG.debug("{} health-check for {}, component now failing: {}", new Object[] {this, entity, getExplanation(state)});
-                        currentFailureStartTime = now;
-                        publishEntityFailedTime = currentFailureStartTime + getConfig(ENTITY_FAILED_STABILIZATION_DELAY).toMilliseconds();
-                    } else {
-                        if (LOG.isTraceEnabled()) LOG.trace("{} health-check for {}, component continuing failing: {}", new Object[] {this, entity, getExplanation(state)});
-                    }
-                }
-                if (setEntityOnFireTime == null) {
-                    setEntityOnFireTime = now + getConfig(SERVICE_ON_FIRE_STABILIZATION_DELAY).toMilliseconds();
-                }
-                currentRecoveryStartTime = null;
-                publishEntityRecoveredTime = null;
-                
-            } else if (state == Lifecycle.RUNNING) {
-                if (lastPublished == LastPublished.FAILED) {
-                    if (currentRecoveryStartTime == null) {
-                        if (LOG.isDebugEnabled()) LOG.debug("{} health-check for {}, component now recovering: {}", new Object[] {this, entity, getExplanation(state)});
-                        currentRecoveryStartTime = now;
-                        publishEntityRecoveredTime = currentRecoveryStartTime + getConfig(ENTITY_RECOVERED_STABILIZATION_DELAY).toMilliseconds();
-                    } else {
-                        if (LOG.isTraceEnabled()) LOG.trace("{} health-check for {}, component continuing recovering: {}", new Object[] {this, entity, getExplanation(state)});
-                    }
-                } else {
-                    if (currentFailureStartTime != null) {
-                        if (LOG.isDebugEnabled()) LOG.debug("{} health-check for {}, component was failing, now healthy: {}", new Object[] {this, entity, getExplanation(state)});
-                    } else {
-                        if (LOG.isTraceEnabled()) LOG.trace("{} health-check for {}, component still healthy: {}", new Object[] {this, entity, getExplanation(state)});
-                    }
-                }
-                currentFailureStartTime = null;
-                publishEntityFailedTime = null;
-                setEntityOnFireTime = null;
-                
-            } else {
-                if (LOG.isTraceEnabled()) LOG.trace("{} health-check for {}, in unconfirmed sate: {}", new Object[] {this, entity, getExplanation(state)});
-            }
-
-            long recomputeIn = Long.MAX_VALUE; // For whether to call recomputeAfterDelay
-            
-            if (publishEntityFailedTime != null) {
-                long delayBeforeCheck = publishEntityFailedTime - now;
-                if (delayBeforeCheck<=0) {
-                    if (LOG.isDebugEnabled()) LOG.debug("{} publishing failed (state={}; currentFailureStartTime={}; now={}", 
-                            new Object[] {this, state, Time.makeDateString(currentFailureStartTime), Time.makeDateString(now)});
-                    Duration republishDelay = getConfig(ENTITY_FAILED_REPUBLISH_TIME);
-                    if (republishDelay == null) {
-                        publishEntityFailedTime = null;
-                    } else {
-                        publishEntityFailedTime = now + republishDelay.toMilliseconds();
-                        recomputeIn = Math.min(recomputeIn, republishDelay.toMilliseconds());
-                    }
-                    lastPublished = LastPublished.FAILED;
-                    entity.emit(HASensors.ENTITY_FAILED, new HASensors.FailureDescriptor(entity, getFailureDescription(now)));
-                } else {
-                    recomputeIn = Math.min(recomputeIn, delayBeforeCheck);
-                }
-            } else if (publishEntityRecoveredTime != null) {
-                long delayBeforeCheck = publishEntityRecoveredTime - now;
-                if (delayBeforeCheck<=0) {
-                    if (LOG.isDebugEnabled()) LOG.debug("{} publishing recovered (state={}; currentRecoveryStartTime={}; now={}", 
-                            new Object[] {this, state, Time.makeDateString(currentRecoveryStartTime), Time.makeDateString(now)});
-                    publishEntityRecoveredTime = null;
-                    lastPublished = LastPublished.RECOVERED;
-                    entity.emit(HASensors.ENTITY_RECOVERED, new HASensors.FailureDescriptor(entity, null));
-                } else {
-                    recomputeIn = Math.min(recomputeIn, delayBeforeCheck);
-                }
-            }
-            
-            if (setEntityOnFireTime != null) {
-                long delayBeforeCheck = setEntityOnFireTime - now;
-                if (delayBeforeCheck<=0) {
-                    if (LOG.isDebugEnabled()) LOG.debug("{} setting on-fire, now that deferred period has passed (state={})", 
-                            new Object[] {this, state});
-                    setEntityOnFireTime = null;
-                    super.setActualState(state);
-                } else {
-                    recomputeIn = Math.min(recomputeIn, delayBeforeCheck);
-                }
-            } else {
-                super.setActualState(state);
-            }
-            
-            if (recomputeIn < Long.MAX_VALUE) {
-                recomputeAfterDelay(recomputeIn);
-            }
-        }
-    }
-
-    protected String getExplanation(Lifecycle state) {
-        Duration serviceFailedStabilizationDelay = getConfig(ENTITY_FAILED_STABILIZATION_DELAY);
-        Duration serviceRecoveredStabilizationDelay = getConfig(ENTITY_RECOVERED_STABILIZATION_DELAY);
-
-        return String.format("location=%s; status=%s; lastPublished=%s; timeNow=%s; "+
-                    "currentFailurePeriod=%s; currentRecoveryPeriod=%s",
-                entity.getLocations(), 
-                (state != null ? state : "<unreported>"),
-                lastPublished,
-                Time.makeDateString(System.currentTimeMillis()),
-                (currentFailureStartTime != null ? getTimeStringSince(currentFailureStartTime) : "<none>") + " (stabilization "+Time.makeTimeStringRounded(serviceFailedStabilizationDelay) + ")",
-                (currentRecoveryStartTime != null ? getTimeStringSince(currentRecoveryStartTime) : "<none>") + " (stabilization "+Time.makeTimeStringRounded(serviceRecoveredStabilizationDelay) + ")");
-    }
-    
-    private String getFailureDescription(long now) {
-        String description = null;
-        Map<String, Object> serviceProblems = entity.getAttribute(Attributes.SERVICE_PROBLEMS);
-        if (serviceProblems!=null && !serviceProblems.isEmpty()) {
-            Entry<String, Object> problem = serviceProblems.entrySet().iterator().next();
-            description = problem.getKey()+": "+problem.getValue();
-            if (serviceProblems.size()>1) {
-                description = serviceProblems.size()+" service problems, including "+description;
-            } else {
-                description = "service problem: "+description;
-            }
-        } else if (Boolean.FALSE.equals(entity.getAttribute(Attributes.SERVICE_UP))) {
-            description = "service not up";
-        } else {
-            description = "service failure detected";
-        }
-        if (publishEntityFailedTime!=null && currentFailureStartTime!=null && publishEntityFailedTime > currentFailureStartTime)
-            description = " (stabilized for "+Duration.of(now - currentFailureStartTime, TimeUnit.MILLISECONDS)+")";
-        return description;
-    }
-    
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    protected void recomputeAfterDelay(long delay) {
-        if (isRunning() && executorQueued.compareAndSet(false, true)) {
-            long now = System.currentTimeMillis();
-            delay = Math.max(0, Math.max(delay, (executorTime + MIN_PERIOD_BETWEEN_EXECS_MILLIS) - now));
-            if (LOG.isTraceEnabled()) LOG.trace("{} scheduling publish in {}ms", this, delay);
-            
-            Runnable job = new Runnable() {
-                @Override public void run() {
-                    try {
-                        executorTime = System.currentTimeMillis();
-                        executorQueued.set(false);
-
-                        onEvent(null);
-                        
-                    } catch (Exception e) {
-                        if (isRunning()) {
-                            LOG.error("Error in enricher "+this+": "+e, e);
-                        } else {
-                            if (LOG.isDebugEnabled()) LOG.debug("Error in enricher "+this+" (but no longer running): "+e, e);
-                        }
-                    } catch (Throwable t) {
-                        LOG.error("Error in enricher "+this+": "+t, t);
-                        throw Exceptions.propagate(t);
-                    }
-                }
-            };
-            
-            ScheduledTask task = new ScheduledTask(MutableMap.of("delay", Duration.of(delay, TimeUnit.MILLISECONDS)), new BasicTask(job));
-            ((EntityInternal)entity).getExecutionContext().submit(task);
-        }
-    }
-    
-    private String getTimeStringSince(Long time) {
-        return time == null ? null : Time.makeTimeStringRounded(System.currentTimeMillis() - time);
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/ha/ServiceReplacer.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/ha/ServiceReplacer.java b/policy/src/main/java/brooklyn/policy/ha/ServiceReplacer.java
deleted file mode 100644
index 6bea1d4..0000000
--- a/policy/src/main/java/brooklyn/policy/ha/ServiceReplacer.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.brooklyn.api.catalog.Catalog;
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
-import org.apache.brooklyn.core.util.config.ConfigBag;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.basic.EntityInternal;
-import brooklyn.entity.basic.ServiceStateLogic.ServiceProblemsLogic;
-import brooklyn.entity.group.StopFailedRuntimeException;
-import brooklyn.entity.trait.MemberReplaceable;
-import brooklyn.event.basic.BasicConfigKey;
-import brooklyn.event.basic.BasicNotificationSensor;
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.exceptions.Exceptions;
-
-import com.google.common.base.Ticker;
-import com.google.common.collect.Lists;
-
-/** attaches to a DynamicCluster and replaces a failed member in response to HASensors.ENTITY_FAILED or other sensor;
- * if this fails, it sets the Cluster state to on-fire */
-@Catalog(name="Service Replacer", description="HA policy for replacing a failed member of a group")
-public class ServiceReplacer extends AbstractPolicy {
-
-    private static final Logger LOG = LoggerFactory.getLogger(ServiceReplacer.class);
-
-    // TODO if there are multiple failures perhaps we should abort quickly
-    
-    public static final BasicNotificationSensor<FailureDescriptor> ENTITY_REPLACEMENT_FAILED = new BasicNotificationSensor<FailureDescriptor>(
-            FailureDescriptor.class, "ha.entityFailed.replacement", "Indicates that an entity replacement attempt has failed");
-
-    @SetFromFlag("setOnFireOnFailure")
-    public static final ConfigKey<Boolean> SET_ON_FIRE_ON_FAILURE = ConfigKeys.newBooleanConfigKey("setOnFireOnFailure", "", true);
-    
-    /** monitors this sensor, by default ENTITY_RESTART_FAILED */
-    @SetFromFlag("failureSensorToMonitor")
-    @SuppressWarnings("rawtypes")
-    public static final ConfigKey<Sensor> FAILURE_SENSOR_TO_MONITOR = new BasicConfigKey<Sensor>(Sensor.class, "failureSensorToMonitor", "", ServiceRestarter.ENTITY_RESTART_FAILED); 
-
-    /** skips replace if replacement has failed this many times failure re-occurs within this time interval */
-    @SetFromFlag("failOnRecurringFailuresInThisDuration")
-    public static final ConfigKey<Long> FAIL_ON_RECURRING_FAILURES_IN_THIS_DURATION = ConfigKeys.newLongConfigKey(
-            "failOnRecurringFailuresInThisDuration", 
-            "abandon replace if replacement has failed many times within this time interval",
-            5*60*1000L);
-
-    /** skips replace if replacement has failed this many times failure re-occurs within this time interval */
-    @SetFromFlag("failOnNumRecurringFailures")
-    public static final ConfigKey<Integer> FAIL_ON_NUM_RECURRING_FAILURES = ConfigKeys.newIntegerConfigKey(
-            "failOnNumRecurringFailures", 
-            "abandon replace if replacement has failed this many times (100% of attempts) within the time interval",
-            5);
-
-    @SetFromFlag("ticker")
-    public static final ConfigKey<Ticker> TICKER = ConfigKeys.newConfigKey(Ticker.class,
-            "ticker", 
-            "A time source (defaults to system-clock, which is almost certainly what's wanted, except in tests)",
-            null);
-
-    protected final List<Long> consecutiveReplacementFailureTimes = Lists.newCopyOnWriteArrayList();
-    
-    public ServiceReplacer() {
-        this(new ConfigBag());
-    }
-    
-    public ServiceReplacer(Map<String,?> flags) {
-        this(new ConfigBag().putAll(flags));
-    }
-    
-    public ServiceReplacer(ConfigBag configBag) {
-        // TODO hierarchy should use ConfigBag, and not change flags
-        super(configBag.getAllConfigMutable());
-    }
-    
-    public ServiceReplacer(Sensor<?> failureSensorToMonitor) {
-        this(new ConfigBag().configure(FAILURE_SENSOR_TO_MONITOR, failureSensorToMonitor));
-    }
-
-    @Override
-    public void setEntity(final EntityLocal entity) {
-        checkArgument(entity instanceof MemberReplaceable, "ServiceReplacer must take a MemberReplaceable, not %s", entity);
-        Sensor<?> failureSensorToMonitor = checkNotNull(getConfig(FAILURE_SENSOR_TO_MONITOR), "failureSensorToMonitor");
-        
-        super.setEntity(entity);
-
-        subscribeToMembers((Group)entity, failureSensorToMonitor, new SensorEventListener<Object>() {
-                @Override public void onEvent(final SensorEvent<Object> event) {
-                    // Must execute in another thread - if we called entity.replaceMember in the event-listener's thread
-                    // then we'd block all other events being delivered to this entity's other subscribers.
-                    // Relies on synchronization of `onDetectedFailure`.
-                    // See same pattern used in ServiceRestarter.
-                    
-                    // TODO Could use BasicExecutionManager.setTaskSchedulerForTag to prevent race of two
-                    // events being received in rapid succession, and onDetectedFailure being executed out-of-order
-                    // for them; or could write events to a blocking queue and have onDetectedFailure read from that.
-                    
-                    if (isRunning()) {
-                        LOG.warn("ServiceReplacer notified; dispatching job for "+entity+" ("+event.getValue()+")");
-                        ((EntityInternal)entity).getExecutionContext().submit(MutableMap.of(), new Runnable() {
-                            @Override public void run() {
-                                onDetectedFailure(event);
-                            }});
-                    } else {
-                        LOG.warn("ServiceReplacer not running, so not acting on failure detected at "+entity+" ("+event.getValue()+", child of "+entity+")");
-                    }
-                }
-            });
-    }
-    
-    // TODO semaphores would be better to allow at-most-one-blocking behaviour
-    protected synchronized void onDetectedFailure(SensorEvent<Object> event) {
-        final Entity failedEntity = event.getSource();
-        final Object reason = event.getValue();
-        
-        if (isSuspended()) {
-            LOG.warn("ServiceReplacer suspended, so not acting on failure detected at "+failedEntity+" ("+reason+", child of "+entity+")");
-            return;
-        }
-
-        if (isRepeatedlyFailingTooMuch()) {
-            LOG.error("ServiceReplacer not acting on failure detected at "+failedEntity+" ("+reason+", child of "+entity+"), because too many recent replacement failures");
-            return;
-        }
-        
-        LOG.warn("ServiceReplacer acting on failure detected at "+failedEntity+" ("+reason+", child of "+entity+")");
-        ((EntityInternal)entity).getManagementSupport().getExecutionContext().submit(MutableMap.of(), new Runnable() {
-
-            @Override
-            public void run() {
-                try {
-                    Entities.invokeEffectorWithArgs(entity, entity, MemberReplaceable.REPLACE_MEMBER, failedEntity.getId()).get();
-                    consecutiveReplacementFailureTimes.clear();
-                } catch (Exception e) {
-                    if (Exceptions.getFirstThrowableOfType(e, StopFailedRuntimeException.class) != null) {
-                        LOG.info("ServiceReplacer: ignoring error reported from stopping failed node "+failedEntity);
-                        return;
-                    }
-                    onReplacementFailed("Replace failure ("+Exceptions.collapseText(e)+") at "+entity+": "+reason);
-                }
-            }
-        });
-    }
-
-    private boolean isRepeatedlyFailingTooMuch() {
-        Integer failOnNumRecurringFailures = getConfig(FAIL_ON_NUM_RECURRING_FAILURES);
-        long failOnRecurringFailuresInThisDuration = getConfig(FAIL_ON_RECURRING_FAILURES_IN_THIS_DURATION);
-        long oldestPermitted = currentTimeMillis() - failOnRecurringFailuresInThisDuration;
-        
-        // trim old ones
-        for (Iterator<Long> iter = consecutiveReplacementFailureTimes.iterator(); iter.hasNext();) {
-            Long timestamp = iter.next();
-            if (timestamp < oldestPermitted) {
-                iter.remove();
-            } else {
-                break;
-            }
-        }
-        
-        return (consecutiveReplacementFailureTimes.size() >= failOnNumRecurringFailures);
-    }
-
-    protected long currentTimeMillis() {
-        Ticker ticker = getConfig(TICKER);
-        return (ticker == null) ? System.currentTimeMillis() : TimeUnit.NANOSECONDS.toMillis(ticker.read());
-    }
-    
-    protected void onReplacementFailed(String msg) {
-        LOG.warn("ServiceReplacer failed for "+entity+": "+msg);
-        consecutiveReplacementFailureTimes.add(currentTimeMillis());
-        
-        if (getConfig(SET_ON_FIRE_ON_FAILURE)) {
-            ServiceProblemsLogic.updateProblemsIndicator(entity, "ServiceReplacer", "replacement failed: "+msg);
-        }
-        entity.emit(ENTITY_REPLACEMENT_FAILED, new FailureDescriptor(entity, msg));
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/ha/ServiceRestarter.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/ha/ServiceRestarter.java b/policy/src/main/java/brooklyn/policy/ha/ServiceRestarter.java
deleted file mode 100644
index ab1359d..0000000
--- a/policy/src/main/java/brooklyn/policy/ha/ServiceRestarter.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.brooklyn.api.catalog.Catalog;
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
-import org.apache.brooklyn.core.util.config.ConfigBag;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.basic.EntityInternal;
-import brooklyn.entity.basic.Lifecycle;
-import brooklyn.entity.basic.ServiceStateLogic;
-import brooklyn.entity.trait.Startable;
-import brooklyn.event.basic.BasicNotificationSensor;
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.javalang.JavaClassNames;
-import brooklyn.util.time.Duration;
-import brooklyn.util.time.Time;
-
-import com.google.common.base.Preconditions;
-
-/** attaches to a SoftwareProcess (or anything Startable, emitting ENTITY_FAILED or other configurable sensor),
- * and invokes restart on failure; 
- * if there is a subsequent failure within a configurable time interval, or if the restart fails,
- * this gives up and emits {@link #ENTITY_RESTART_FAILED} 
- */
-@Catalog(name="Service Restarter", description="HA policy for restarting a service automatically, "
-        + "and for emitting an events if the service repeatedly fails")
-public class ServiceRestarter extends AbstractPolicy {
-
-    private static final Logger LOG = LoggerFactory.getLogger(ServiceRestarter.class);
-
-    public static final BasicNotificationSensor<FailureDescriptor> ENTITY_RESTART_FAILED = new BasicNotificationSensor<FailureDescriptor>(
-            FailureDescriptor.class, "ha.entityFailed.restart", "Indicates that an entity restart attempt has failed");
-
-    /** skips retry if a failure re-occurs within this time interval */
-    @SetFromFlag("failOnRecurringFailuresInThisDuration")
-    public static final ConfigKey<Duration> FAIL_ON_RECURRING_FAILURES_IN_THIS_DURATION = ConfigKeys.newConfigKey(
-            Duration.class, 
-            "failOnRecurringFailuresInThisDuration", 
-            "Reports entity as failed if it fails two or more times in this time window", 
-            Duration.minutes(3));
-
-    @SetFromFlag("setOnFireOnFailure")
-    public static final ConfigKey<Boolean> SET_ON_FIRE_ON_FAILURE = ConfigKeys.newBooleanConfigKey("setOnFireOnFailure", "", true);
-
-    /** monitors this sensor, by default ENTITY_FAILED */
-    @SetFromFlag("failureSensorToMonitor")
-    @SuppressWarnings({ "rawtypes", "unchecked" })
-    public static final ConfigKey<Sensor<?>> FAILURE_SENSOR_TO_MONITOR = (ConfigKey) ConfigKeys.newConfigKey(Sensor.class, "failureSensorToMonitor", "", HASensors.ENTITY_FAILED); 
-    
-    protected final AtomicReference<Long> lastFailureTime = new AtomicReference<Long>();
-
-    public ServiceRestarter() {
-        this(new ConfigBag());
-    }
-    
-    public ServiceRestarter(Map<String,?> flags) {
-        this(new ConfigBag().putAll(flags));
-    }
-    
-    public ServiceRestarter(ConfigBag configBag) {
-        // TODO hierarchy should use ConfigBag, and not change flags
-        super(configBag.getAllConfigMutable());
-        uniqueTag = JavaClassNames.simpleClassName(getClass())+":"+getConfig(FAILURE_SENSOR_TO_MONITOR).getName();
-    }
-    
-    public ServiceRestarter(Sensor<?> failureSensorToMonitor) {
-        this(new ConfigBag().configure(FAILURE_SENSOR_TO_MONITOR, failureSensorToMonitor));
-    }
-
-    @Override
-    public void setEntity(final EntityLocal entity) {
-        Preconditions.checkArgument(entity instanceof Startable, "Restarter must take a Startable, not "+entity);
-        
-        super.setEntity(entity);
-        
-        subscribe(entity, getConfig(FAILURE_SENSOR_TO_MONITOR), new SensorEventListener<Object>() {
-                @Override public void onEvent(final SensorEvent<Object> event) {
-                    // Must execute in another thread - if we called entity.restart in the event-listener's thread
-                    // then we'd block all other events being delivered to this entity's other subscribers.
-                    // Relies on synchronization of `onDetectedFailure`.
-                    // See same pattern used in ServiceReplacer.
-
-                    // TODO Could use BasicExecutionManager.setTaskSchedulerForTag to prevent race of two
-                    // events being received in rapid succession, and onDetectedFailure being executed out-of-order
-                    // for them; or could write events to a blocking queue and have onDetectedFailure read from that.
-                    
-                    if (isRunning()) {
-                        LOG.info("ServiceRestarter notified; dispatching job for "+entity+" ("+event.getValue()+")");
-                        ((EntityInternal)entity).getExecutionContext().submit(MutableMap.of(), new Runnable() {
-                            @Override public void run() {
-                                onDetectedFailure(event);
-                            }});
-                    } else {
-                        LOG.warn("ServiceRestarter not running, so not acting on failure detected at "+entity+" ("+event.getValue()+")");
-                    }
-                }
-            });
-    }
-    
-    // TODO semaphores would be better to allow at-most-one-blocking behaviour
-    // FIXME as this is called in message-dispatch (single threaded) we should do most of this in a new submitted task
-    // (as has been done in ServiceReplacer)
-    protected synchronized void onDetectedFailure(SensorEvent<Object> event) {
-        if (isSuspended()) {
-            LOG.warn("ServiceRestarter suspended, so not acting on failure detected at "+entity+" ("+event.getValue()+")");
-            return;
-        }
-
-        LOG.warn("ServiceRestarter acting on failure detected at "+entity+" ("+event.getValue()+")");
-        long current = System.currentTimeMillis();
-        Long last = lastFailureTime.getAndSet(current);
-        long elapsed = last==null ? -1 : current-last;
-        if (elapsed>=0 && elapsed <= getConfig(FAIL_ON_RECURRING_FAILURES_IN_THIS_DURATION).toMilliseconds()) {
-            onRestartFailed("Restart failure (failed again after "+Time.makeTimeStringRounded(elapsed)+") at "+entity+": "+event.getValue());
-            return;
-        }
-        try {
-            ServiceStateLogic.setExpectedState(entity, Lifecycle.STARTING);
-            Entities.invokeEffector(entity, entity, Startable.RESTART).get();
-        } catch (Exception e) {
-            onRestartFailed("Restart failure (error "+e+") at "+entity+": "+event.getValue());
-        }
-    }
-
-    protected void onRestartFailed(String msg) {
-        LOG.warn("ServiceRestarter failed for "+entity+": "+msg);
-        if (getConfig(SET_ON_FIRE_ON_FAILURE)) {
-            ServiceStateLogic.setExpectedState(entity, Lifecycle.ON_FIRE);
-        }
-        entity.emit(ENTITY_RESTART_FAILED, new FailureDescriptor(entity, msg));
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/ha/SshMachineFailureDetector.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/ha/SshMachineFailureDetector.java b/policy/src/main/java/brooklyn/policy/ha/SshMachineFailureDetector.java
deleted file mode 100644
index 1fa9982..0000000
--- a/policy/src/main/java/brooklyn/policy/ha/SshMachineFailureDetector.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.ha;
-
-import java.util.Map;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.brooklyn.api.catalog.Catalog;
-import org.apache.brooklyn.core.util.internal.ssh.SshTool;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.event.basic.BasicNotificationSensor;
-
-import org.apache.brooklyn.location.basic.Machines;
-import org.apache.brooklyn.location.basic.SshMachineLocation;
-
-import brooklyn.policy.ha.HASensors.FailureDescriptor;
-import brooklyn.util.exceptions.Exceptions;
-import brooklyn.util.guava.Maybe;
-import brooklyn.util.time.Duration;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-
-@Catalog(name="Ssh Connectivity Failure Detector", description="HA policy for monitoring an SshMachine, "
-        + "emitting an event if the connection is lost/restored")
-public class SshMachineFailureDetector extends AbstractFailureDetector {
-    private static final Logger LOG = LoggerFactory.getLogger(SshMachineFailureDetector.class);
-    public static final String DEFAULT_UNIQUE_TAG = "failureDetector.sshMachine.tag";
-
-    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_FAILED = HASensors.CONNECTION_FAILED;
-
-    public static final BasicNotificationSensor<FailureDescriptor> CONNECTION_RECOVERED = HASensors.CONNECTION_RECOVERED;
-
-    public static final ConfigKey<Duration> CONNECT_TIMEOUT = ConfigKeys.newDurationConfigKey(
-            "ha.sshConnection.timeout", "How long to wait for conneciton before declaring failure", Duration.TEN_SECONDS);
-
-    @Override
-    public void init() {
-        super.init();
-        if (config().getRaw(SENSOR_FAILED).isAbsent()) {
-            config().set(SENSOR_FAILED, CONNECTION_FAILED);
-        }
-        if (config().getRaw(SENSOR_RECOVERED).isAbsent()) {
-            config().set(SENSOR_RECOVERED, CONNECTION_RECOVERED);
-        }
-        if (config().getRaw(POLL_PERIOD).isAbsent()) {
-            config().set(POLL_PERIOD, Duration.ONE_MINUTE);
-        }
-        uniqueTag = DEFAULT_UNIQUE_TAG;
-    }
-
-    @Override
-    protected CalculatedStatus calculateStatus() {
-        Maybe<SshMachineLocation> sshMachineOption = Machines.findUniqueSshMachineLocation(entity.getLocations());
-        if (sshMachineOption.isPresent()) {
-            SshMachineLocation sshMachine = sshMachineOption.get();
-            try {
-                Duration timeout = config().get(CONNECT_TIMEOUT);
-                Map<String, ?> flags = ImmutableMap.of(
-                        SshTool.PROP_CONNECT_TIMEOUT.getName(), timeout.toMilliseconds(),
-                        SshTool.PROP_SESSION_TIMEOUT.getName(), timeout.toMilliseconds(),
-                        SshTool.PROP_SSH_TRIES.getName(), 1);
-                int exitCode = sshMachine.execCommands(flags, SshMachineFailureDetector.class.getName(), ImmutableList.of("exit"));
-                return new BasicCalculatedStatus(exitCode == 0, sshMachine.toString());
-            } catch (Exception e) {
-                Exceptions.propagateIfFatal(e);
-                boolean isFirstFailure = lastPublished != LastPublished.FAILED && currentFailureStartTime == null;
-                if (isFirstFailure) {
-                    if (LOG.isDebugEnabled()) {
-                        LOG.debug("Failed connecting to machine " + sshMachine, e);
-                    }
-                } else {
-                    if (LOG.isTraceEnabled()) {
-                        LOG.trace("Failed connecting to machine " + sshMachine, e);
-                    }
-                }
-                return new BasicCalculatedStatus(false, e.getMessage());
-            }
-        } else {
-            return new BasicCalculatedStatus(true, "no machine started, not complaining");
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableContainer.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableContainer.java b/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableContainer.java
deleted file mode 100644
index 0039685..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableContainer.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import java.util.Set;
-
-import org.apache.brooklyn.api.entity.Entity;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.AbstractGroup;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.event.basic.BasicNotificationSensor;
-import brooklyn.util.collections.QuorumCheck;
-import brooklyn.util.collections.QuorumCheck.QuorumChecks;
-
-/**
- * Contains worker items that can be moved between this container and others to effect load balancing.
- * Membership of a balanceable container does not imply a parent-child relationship in the Brooklyn
- * management sense.
- */
-public interface BalanceableContainer<ItemType extends Movable> extends Entity, AbstractGroup {
-    
-    public static BasicNotificationSensor<Entity> ITEM_ADDED = new BasicNotificationSensor<Entity>(
-            Entity.class, "balanceablecontainer.item.added", "Movable item added to balanceable container");
-    public static BasicNotificationSensor<Entity> ITEM_REMOVED = new BasicNotificationSensor<Entity>(
-            Entity.class, "balanceablecontainer.item.removed", "Movable item removed from balanceable container");
-    
-    public static final ConfigKey<QuorumCheck> UP_QUORUM_CHECK = ConfigKeys.newConfigKeyWithDefault(AbstractGroup.UP_QUORUM_CHECK, 
-        "Up check from members; default one for container overrides usual check to always return true, "
-        + "i.e. not block service up simply because the container is empty or something in the container has failed",
-        QuorumChecks.alwaysTrue());
-
-    public Set<ItemType> getBalanceableItems();
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceablePoolModel.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceablePoolModel.java b/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceablePoolModel.java
deleted file mode 100644
index 33f9e0b..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceablePoolModel.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import java.util.Map;
-import java.util.Set;
-
-import org.apache.brooklyn.api.location.Location;
-
-/**
- * Captures the state of a balanceable cluster of containers and all their constituent items, including workrates,
- * for consumption by a {@link BalancingStrategy}.
- */
-public interface BalanceablePoolModel<ContainerType, ItemType> {
-    
-    // Attributes of the pool.
-    public String getName();
-    public int getPoolSize();
-    public Set<ContainerType> getPoolContents();
-    public double getPoolLowThreshold();
-    public double getPoolHighThreshold();
-    public double getCurrentPoolWorkrate();
-    public boolean isHot();
-    public boolean isCold();
-    
-    
-    // Attributes of containers and items.
-    public String getName(ContainerType container);
-    public Location getLocation(ContainerType container);
-    public double getLowThreshold(ContainerType container); // -1 for not known / invalid
-    public double getHighThreshold(ContainerType container); // -1 for not known / invalid
-    public double getTotalWorkrate(ContainerType container); // -1 for not known / invalid
-    public Map<ContainerType, Double> getContainerWorkrates(); // contains -1 for items which are unknown
-    /** contains -1 instead of actual item workrate, for items which cannot be moved */
-    // @Nullable("null if the node is prevented from reporting and/or being adjusted, or has no data yet")
-    public Map<ItemType, Double> getItemWorkrates(ContainerType container);
-    public boolean isItemMoveable(ItemType item);
-    public boolean isItemAllowedIn(ItemType item, Location location);
-    
-    // Mutators for keeping the model in-sync with the observed world
-    public void onContainerAdded(ContainerType newContainer, double lowThreshold, double highThreshold);
-    public void onContainerRemoved(ContainerType oldContainer);
-    public void onItemAdded(ItemType item, ContainerType parentContainer);
-    public void onItemAdded(ItemType item, ContainerType parentContainer, boolean immovable);
-    public void onItemRemoved(ItemType item);
-    public void onItemWorkrateUpdated(ItemType item, double newValue);
-    public void onItemMoved(ItemType item, ContainerType targetContainer);
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableWorkerPool.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableWorkerPool.java b/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableWorkerPool.java
deleted file mode 100644
index c8d3a8b..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableWorkerPool.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import java.io.Serializable;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
-
-import brooklyn.entity.trait.Resizable;
-import brooklyn.event.basic.BasicNotificationSensor;
-
-/**
- * Represents an elastic group of "container" entities, each of which is capable of hosting "item" entities that perform
- * work and consume the container's available resources (e.g. CPU or bandwidth). Auto-scaling and load-balancing policies can
- * be attached to this pool to provide dynamic elasticity based on workrates reported by the individual item entities.
- * <p>
- * The containers must be "up" in order to receive work, thus they must NOT follow the default enricher pattern
- * for groups which says that the group must be up to receive work.
- */
-@ImplementedBy(BalanceableWorkerPoolImpl.class)
-public interface BalanceableWorkerPool extends Entity, Resizable {
-
-    // FIXME Asymmetry between loadbalancing and followTheSun: ITEM_ADDED and ITEM_REMOVED in loadbalancing
-    // are of type ContainerItemPair, but in followTheSun it is just the `Entity item`.
-    
-    /** Encapsulates an item and a container; emitted for {@code ITEM_ADDED}, {@code ITEM_REMOVED} and
-     * {@code ITEM_MOVED} sensors.
-     */
-    public static class ContainerItemPair implements Serializable {
-        private static final long serialVersionUID = 1L;
-        public final BalanceableContainer<?> container;
-        public final Entity item;
-        
-        public ContainerItemPair(BalanceableContainer<?> container, Entity item) {
-            this.container = container;
-            this.item = checkNotNull(item);
-        }
-        
-        @Override
-        public String toString() {
-            return ""+item+" @ "+container;
-        }
-    }
-    
-    // Pool constituent notifications.
-    public static BasicNotificationSensor<Entity> CONTAINER_ADDED = new BasicNotificationSensor<Entity>(
-        Entity.class, "balanceablepool.container.added", "Container added to balanceable pool");
-    public static BasicNotificationSensor<Entity> CONTAINER_REMOVED = new BasicNotificationSensor<Entity>(
-        Entity.class, "balanceablepool.container.removed", "Container removed from balanceable pool");
-    public static BasicNotificationSensor<ContainerItemPair> ITEM_ADDED = new BasicNotificationSensor<ContainerItemPair>(
-        ContainerItemPair.class, "balanceablepool.item.added", "Item added to balanceable pool");
-    public static BasicNotificationSensor<ContainerItemPair> ITEM_REMOVED = new BasicNotificationSensor<ContainerItemPair>(
-        ContainerItemPair.class, "balanceablepool.item.removed", "Item removed from balanceable pool");
-    public static BasicNotificationSensor<ContainerItemPair> ITEM_MOVED = new BasicNotificationSensor<ContainerItemPair>(
-        ContainerItemPair.class, "balanceablepool.item.moved", "Item moved in balanceable pool to the given container");
-    
-    public void setResizable(Resizable resizable);
-    
-    public void setContents(Group containerGroup, Group itemGroup);
-    
-    public Group getContainerGroup();
-    
-    public Group getItemGroup();
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableWorkerPoolImpl.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableWorkerPoolImpl.java b/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableWorkerPoolImpl.java
deleted file mode 100644
index b3f9633..0000000
--- a/policy/src/main/java/brooklyn/policy/loadbalancing/BalanceableWorkerPoolImpl.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.loadbalancing;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.entity.basic.AbstractEntity;
-import brooklyn.entity.basic.AbstractGroup;
-import brooklyn.entity.trait.Resizable;
-import brooklyn.entity.trait.Startable;
-
-/**
- * @see BalanceableWorkerPool
- */
-public class BalanceableWorkerPoolImpl extends AbstractEntity implements BalanceableWorkerPool {
-
-    // FIXME Asymmetry between loadbalancing and followTheSun: ITEM_ADDED and ITEM_REMOVED in loadbalancing
-    // are of type ContainerItemPair, but in followTheSun it is just the `Entity item`.
-    
-    private static final Logger LOG = LoggerFactory.getLogger(BalanceableWorkerPool.class);
-    
-    private Group containerGroup;
-    private Group itemGroup;
-    private Resizable resizable;
-    
-    private final Set<Entity> containers = Collections.synchronizedSet(new HashSet<Entity>());
-    private final Set<Entity> items = Collections.synchronizedSet(new HashSet<Entity>());
-    
-    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
-        @Override
-        public void onEvent(SensorEvent<Object> event) {
-            if (LOG.isTraceEnabled()) LOG.trace("{} received event {}", BalanceableWorkerPoolImpl.this, event);
-            Entity source = event.getSource();
-            Object value = event.getValue();
-            Sensor<?> sensor = event.getSensor();
-            
-            if (sensor.equals(AbstractGroup.MEMBER_ADDED)) {
-                if (source.equals(containerGroup)) {
-                    onContainerAdded((BalanceableContainer<?>) value);
-                } else if (source.equals(itemGroup)) {
-                    onItemAdded((Entity)value);
-                } else {
-                    throw new IllegalStateException("unexpected event source="+source);
-                }
-            } else if (sensor.equals(AbstractGroup.MEMBER_REMOVED)) {
-                if (source.equals(containerGroup)) {
-                    onContainerRemoved((BalanceableContainer<?>) value);
-                } else if (source.equals(itemGroup)) {
-                    onItemRemoved((Entity) value);
-                } else {
-                    throw new IllegalStateException("unexpected event source="+source);
-                }
-            } else if (sensor.equals(Startable.SERVICE_UP)) {
-                // TODO What if start has failed? Is there a sensor to indicate that?
-                if ((Boolean)value) {
-                    onContainerUp((BalanceableContainer<?>) source);
-                } else {
-                    onContainerDown((BalanceableContainer<?>) source);
-                }
-            } else if (sensor.equals(Movable.CONTAINER)) {
-                onItemMoved(source, (BalanceableContainer<?>) value);
-            } else {
-                throw new IllegalStateException("Unhandled event type "+sensor+": "+event);
-            }
-        }
-    };
-    
-    public BalanceableWorkerPoolImpl() {
-    }
-
-    @Override
-    public void setResizable(Resizable resizable) {
-        this.resizable = resizable;
-    }
-    
-    @Override
-    public void setContents(Group containerGroup, Group itemGroup) {
-        this.containerGroup = containerGroup;
-        this.itemGroup = itemGroup;
-        if (resizable == null && containerGroup instanceof Resizable) resizable = (Resizable) containerGroup;
-        
-        subscribe(containerGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
-        subscribe(containerGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
-        subscribe(itemGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
-        subscribe(itemGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
-        
-        // Process extant containers and items
-        for (Entity existingContainer : containerGroup.getMembers()) {
-            onContainerAdded((BalanceableContainer<?>)existingContainer);
-        }
-        for (Entity existingItem : itemGroup.getMembers()) {
-            onItemAdded(existingItem);
-        }
-    }
-    
-    @Override
-    public Group getContainerGroup() {
-        return containerGroup;
-    }
-    
-    @Override
-    public Group getItemGroup() {
-        return itemGroup;
-    }
-
-    @Override
-    public Integer getCurrentSize() {
-        return containerGroup.getCurrentSize();
-    }
-    
-    @Override
-    public Integer resize(Integer desiredSize) {
-        if (resizable != null) return resizable.resize(desiredSize);
-        
-        throw new UnsupportedOperationException("Container group is not resizable, and no resizable supplied: "+containerGroup+" of type "+(containerGroup != null ? containerGroup.getClass().getCanonicalName() : null));
-    }
-    
-    private void onContainerAdded(BalanceableContainer<?> newContainer) {
-        subscribe(newContainer, Startable.SERVICE_UP, eventHandler);
-        if (!(newContainer instanceof Startable) || Boolean.TRUE.equals(newContainer.getAttribute(Startable.SERVICE_UP))) {
-            onContainerUp(newContainer);
-        }
-    }
-    
-    private void onContainerUp(BalanceableContainer<?> newContainer) {
-        if (containers.add(newContainer)) {
-            emit(CONTAINER_ADDED, newContainer);
-        }
-    }
-    
-    private void onContainerDown(BalanceableContainer<?> oldContainer) {
-        if (containers.remove(oldContainer)) {
-            emit(CONTAINER_REMOVED, oldContainer);
-        }
-    }
-    
-    private void onContainerRemoved(BalanceableContainer<?> oldContainer) {
-        unsubscribe(oldContainer);
-        onContainerDown(oldContainer);
-    }
-    
-    private void onItemAdded(Entity item) {
-        if (items.add(item)) {
-            subscribe(item, Movable.CONTAINER, eventHandler);
-            emit(ITEM_ADDED, new ContainerItemPair(item.getAttribute(Movable.CONTAINER), item));
-        }
-    }
-    
-    private void onItemRemoved(Entity item) {
-        if (items.remove(item)) {
-            unsubscribe(item);
-            emit(ITEM_REMOVED, new ContainerItemPair(null, item));
-        }
-    }
-    
-    private void onItemMoved(Entity item, BalanceableContainer<?> container) {
-        emit(ITEM_MOVED, new ContainerItemPair(container, item));
-    }
-}


[23/24] incubator-brooklyn git commit: This closes #831

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


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

Branch: refs/heads/master
Commit: bf2cfcdcc5dfb7efdbaa5e70df2ebd597ad9ac09
Parents: 3a13452 ee1a20a
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Tue Aug 18 12:03:59 2015 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Tue Aug 18 12:03:59 2015 +0100

----------------------------------------------------------------------
 .../location/access/BrooklynAccessUtils.java    |  52 ++++---
 .../access/BrooklynAccessUtilsTest.java         | 138 +++++++++++++++++++
 2 files changed, 170 insertions(+), 20 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/bf2cfcdc/core/src/main/java/org/apache/brooklyn/location/access/BrooklynAccessUtils.java
----------------------------------------------------------------------


[22/24] incubator-brooklyn git commit: This closes #808

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


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

Branch: refs/heads/master
Commit: 3a13452e4067f3fe82721d686278f61b74ad9c42
Parents: 42d9871 d30ff59
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Tue Aug 18 12:02:46 2015 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Tue Aug 18 12:02:46 2015 +0100

----------------------------------------------------------------------
 .../yaml/example_yaml/appserver-w-policy.yaml   |    2 +-
 .../vanilla-bash-netcat-restarter.yaml          |    4 +-
 .../vanilla-bash-netcat-w-client.yaml           |    4 +-
 ...followthesun.DefaultFollowTheSunModel$1.html |    6 +-
 .../brooklyn/demo/CumulusRDFApplication.java    |    6 +-
 .../demo/HighAvailabilityCassandraCluster.java  |    6 +-
 .../brooklyn/demo/ResilientMongoDbApp.java      |    6 +-
 .../brooklyn/demo/RiakClusterExample.java       |    4 +-
 .../brooklyn/demo/WideAreaCassandraCluster.java |    6 +-
 .../brooklyn/demo/ha-cassandra-cluster.yaml     |    6 +-
 .../demo/wide-area-cassandra-cluster.yaml       |    6 +-
 .../demo/WebClusterDatabaseExample.java         |    2 +-
 .../demo/WebClusterDatabaseExampleApp.java      |    3 +-
 .../demo/WebClusterDatabaseExampleGroovy.groovy |    5 +-
 .../apache/brooklyn/demo/WebClusterExample.java |    2 +-
 ...lusterDatabaseExampleAppIntegrationTest.java |    2 +-
 .../policy/autoscaling/AutoScalerPolicy.java    | 1090 -----------------
 .../autoscaling/MaxPoolSizeReachedEvent.java    |  103 --
 .../policy/autoscaling/ResizeOperator.java      |   31 -
 .../policy/autoscaling/SizeHistory.java         |  166 ---
 .../followthesun/DefaultFollowTheSunModel.java  |  328 ------
 .../policy/followthesun/FollowTheSunModel.java  |   56 -
 .../followthesun/FollowTheSunParameters.java    |   95 --
 .../policy/followthesun/FollowTheSunPolicy.java |  280 -----
 .../policy/followthesun/FollowTheSunPool.java   |   75 --
 .../followthesun/FollowTheSunPoolImpl.java      |  178 ---
 .../followthesun/FollowTheSunStrategy.java      |  161 ---
 .../policy/followthesun/WeightedObject.java     |   71 --
 .../policy/ha/AbstractFailureDetector.java      |  361 ------
 .../policy/ha/ConditionalSuspendPolicy.java     |  103 --
 .../policy/ha/ConnectionFailureDetector.java    |  126 --
 .../main/java/brooklyn/policy/ha/HASensors.java |   62 -
 .../policy/ha/ServiceFailureDetector.java       |  340 ------
 .../brooklyn/policy/ha/ServiceReplacer.java     |  214 ----
 .../brooklyn/policy/ha/ServiceRestarter.java    |  163 ---
 .../policy/ha/SshMachineFailureDetector.java    |  102 --
 .../loadbalancing/BalanceableContainer.java     |   51 -
 .../loadbalancing/BalanceablePoolModel.java     |   64 -
 .../loadbalancing/BalanceableWorkerPool.java    |   84 --
 .../BalanceableWorkerPoolImpl.java              |  185 ---
 .../policy/loadbalancing/BalancingStrategy.java |  622 ----------
 .../DefaultBalanceablePoolModel.java            |  280 -----
 .../loadbalancing/ItemsInContainersGroup.java   |   52 -
 .../ItemsInContainersGroupImpl.java             |  148 ---
 .../loadbalancing/LoadBalancingPolicy.java      |  343 ------
 .../loadbalancing/LocationConstraint.java       |   28 -
 .../brooklyn/policy/loadbalancing/Movable.java  |   51 -
 .../policy/loadbalancing/PolicyUtilForPool.java |   96 --
 .../policy/autoscaling/AutoScalerPolicy.java    | 1092 ++++++++++++++++++
 .../autoscaling/MaxPoolSizeReachedEvent.java    |  103 ++
 .../policy/autoscaling/ResizeOperator.java      |   31 +
 .../policy/autoscaling/SizeHistory.java         |  166 +++
 .../followthesun/DefaultFollowTheSunModel.java  |  328 ++++++
 .../policy/followthesun/FollowTheSunModel.java  |   56 +
 .../followthesun/FollowTheSunParameters.java    |   95 ++
 .../policy/followthesun/FollowTheSunPolicy.java |  282 +++++
 .../policy/followthesun/FollowTheSunPool.java   |   75 ++
 .../followthesun/FollowTheSunPoolImpl.java      |  178 +++
 .../followthesun/FollowTheSunStrategy.java      |  161 +++
 .../policy/followthesun/WeightedObject.java     |   71 ++
 .../policy/ha/AbstractFailureDetector.java      |  363 ++++++
 .../policy/ha/ConditionalSuspendPolicy.java     |  103 ++
 .../policy/ha/ConnectionFailureDetector.java    |  128 ++
 .../apache/brooklyn/policy/ha/HASensors.java    |   62 +
 .../policy/ha/ServiceFailureDetector.java       |  340 ++++++
 .../brooklyn/policy/ha/ServiceReplacer.java     |  216 ++++
 .../brooklyn/policy/ha/ServiceRestarter.java    |  165 +++
 .../policy/ha/SshMachineFailureDetector.java    |  103 ++
 .../loadbalancing/BalanceableContainer.java     |   51 +
 .../loadbalancing/BalanceablePoolModel.java     |   64 +
 .../loadbalancing/BalanceableWorkerPool.java    |   84 ++
 .../BalanceableWorkerPoolImpl.java              |  185 +++
 .../policy/loadbalancing/BalancingStrategy.java |  622 ++++++++++
 .../DefaultBalanceablePoolModel.java            |  280 +++++
 .../loadbalancing/ItemsInContainersGroup.java   |   52 +
 .../ItemsInContainersGroupImpl.java             |  148 +++
 .../loadbalancing/LoadBalancingPolicy.java      |  344 ++++++
 .../loadbalancing/LocationConstraint.java       |   28 +
 .../brooklyn/policy/loadbalancing/Movable.java  |   51 +
 .../policy/loadbalancing/PolicyUtilForPool.java |   96 ++
 .../autoscaling/AutoScalerPolicyMetricTest.java |  274 -----
 .../autoscaling/AutoScalerPolicyRebindTest.java |  137 ---
 .../AutoScalerPolicyReconfigurationTest.java    |  190 ---
 .../autoscaling/AutoScalerPolicyTest.java       |  649 -----------
 .../autoscaling/LocallyResizableEntity.java     |   73 --
 .../AbstractFollowTheSunPolicyTest.java         |  239 ----
 .../followthesun/FollowTheSunModelTest.java     |  195 ----
 .../FollowTheSunPolicySoakTest.java             |  274 -----
 .../followthesun/FollowTheSunPolicyTest.java    |  307 -----
 .../ha/ConnectionFailureDetectorTest.java       |  303 -----
 .../brooklyn/policy/ha/HaPolicyRebindTest.java  |  173 ---
 ...ServiceFailureDetectorStabilizationTest.java |  234 ----
 .../policy/ha/ServiceFailureDetectorTest.java   |  408 -------
 .../brooklyn/policy/ha/ServiceReplacerTest.java |  340 ------
 .../policy/ha/ServiceRestarterTest.java         |  190 ---
 .../AbstractLoadBalancingPolicyTest.java        |  253 ----
 .../BalanceableWorkerPoolTest.java              |  133 ---
 .../ItemsInContainersGroupTest.java             |  189 ---
 .../loadbalancing/LoadBalancingModelTest.java   |  115 --
 .../LoadBalancingPolicyConcurrencyTest.java     |  211 ----
 .../LoadBalancingPolicySoakTest.java            |  273 -----
 .../loadbalancing/LoadBalancingPolicyTest.java  |  397 -------
 .../loadbalancing/MockContainerEntity.java      |   61 -
 .../loadbalancing/MockContainerEntityImpl.java  |  208 ----
 .../policy/loadbalancing/MockItemEntity.java    |   46 -
 .../loadbalancing/MockItemEntityImpl.java       |  113 --
 .../autoscaling/AutoScalerPolicyMetricTest.java |  274 +++++
 .../autoscaling/AutoScalerPolicyRebindTest.java |  137 +++
 .../AutoScalerPolicyReconfigurationTest.java    |  190 +++
 .../autoscaling/AutoScalerPolicyTest.java       |  649 +++++++++++
 .../autoscaling/LocallyResizableEntity.java     |   73 ++
 .../AbstractFollowTheSunPolicyTest.java         |  239 ++++
 .../followthesun/FollowTheSunModelTest.java     |  195 ++++
 .../FollowTheSunPolicySoakTest.java             |  274 +++++
 .../followthesun/FollowTheSunPolicyTest.java    |  307 +++++
 .../ha/ConnectionFailureDetectorTest.java       |  303 +++++
 .../brooklyn/policy/ha/HaPolicyRebindTest.java  |  173 +++
 ...ServiceFailureDetectorStabilizationTest.java |  234 ++++
 .../policy/ha/ServiceFailureDetectorTest.java   |  407 +++++++
 .../brooklyn/policy/ha/ServiceReplacerTest.java |  340 ++++++
 .../policy/ha/ServiceRestarterTest.java         |  190 +++
 .../AbstractLoadBalancingPolicyTest.java        |  253 ++++
 .../BalanceableWorkerPoolTest.java              |  133 +++
 .../ItemsInContainersGroupTest.java             |  189 +++
 .../loadbalancing/LoadBalancingModelTest.java   |  113 ++
 .../LoadBalancingPolicyConcurrencyTest.java     |  211 ++++
 .../LoadBalancingPolicySoakTest.java            |  273 +++++
 .../loadbalancing/LoadBalancingPolicyTest.java  |  397 +++++++
 .../loadbalancing/MockContainerEntity.java      |   61 +
 .../loadbalancing/MockContainerEntityImpl.java  |  208 ++++
 .../policy/loadbalancing/MockItemEntity.java    |   46 +
 .../loadbalancing/MockItemEntityImpl.java       |  113 ++
 .../webapp/TomcatAutoScalerPolicyTest.java      |    3 +-
 .../app/ClusterWebServerDatabaseSample.java     |    2 +-
 .../BrooklynYamlTypeInstantiatorTest.java       |    2 +-
 .../brooklyn/JavaWebAppsIntegrationTest.java    |    4 +-
 .../brooklyn/catalog/CatalogYamlCombiTest.java  |    2 +-
 .../java-web-app-and-db-with-policy.yaml        |    2 +-
 .../resources/vanilla-bash-netcat-w-client.yaml |    4 +-
 .../main/resources/brooklyn/default.catalog.bom |   10 +-
 .../src/test/resources/couchbase-w-loadgen.yaml |    2 +-
 .../brooklyn/qa/load/SimulatedTheeTierApp.java  |    2 +-
 .../qa/longevity/webcluster/WebClusterApp.java  |    2 +-
 .../brooklyn/rest/domain/ConfigSummary.java     |    2 +-
 .../rest/resources/CatalogResourceTest.java     |    2 +-
 145 files changed, 12162 insertions(+), 12146 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a13452e/docs/guide/yaml/example_yaml/appserver-w-policy.yaml
----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a13452e/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExample.java
----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a13452e/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleApp.java
----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a13452e/examples/simple-web-cluster/src/test/java/org/apache/brooklyn/demo/RebindWebClusterDatabaseExampleAppIntegrationTest.java
----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a13452e/usage/archetypes/quickstart/src/brooklyn-sample/src/main/java/com/acme/sample/brooklyn/sample/app/ClusterWebServerDatabaseSample.java
----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a13452e/usage/camp/src/test/resources/java-web-app-and-db-with-policy.yaml
----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a13452e/usage/qa/src/main/java/org/apache/brooklyn/qa/load/SimulatedTheeTierApp.java
----------------------------------------------------------------------


[16/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicy.java b/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicy.java
new file mode 100644
index 0000000..98d814f
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicy.java
@@ -0,0 +1,1092 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.autoscaling;
+
+import static brooklyn.util.JavaGroovyEquivalents.groovyTruth;
+import static com.google.common.base.Preconditions.checkNotNull;
+import groovy.lang.Closure;
+
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.api.catalog.Catalog;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.event.AttributeSensor;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.policy.PolicySpec;
+import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+import org.apache.brooklyn.core.util.flags.TypeCoercions;
+import org.apache.brooklyn.core.util.task.Tasks;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.BrooklynTaskTags;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.trait.Resizable;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.event.basic.BasicNotificationSensor;
+
+import org.apache.brooklyn.policy.autoscaling.SizeHistory.WindowSummary;
+import org.apache.brooklyn.policy.loadbalancing.LoadBalancingPolicy;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.reflect.TypeToken;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+
+/**
+ * Policy that is attached to a {@link Resizable} entity and dynamically adjusts its size in response to
+ * emitted {@code POOL_COLD} and {@code POOL_HOT} events. Alternatively, the policy can be configured to
+ * keep a given metric within a required range.
+ * <p>
+ * TThis policy does not itself determine whether the pool is hot or cold, but instead relies on these 
+ * events being emitted by the monitored entity itself, or by another policy that is attached to it; see, 
+ * for example, {@link LoadBalancingPolicy}.)
+ */
+@SuppressWarnings({"rawtypes", "unchecked"})
+@Catalog(name="Auto-scaler", description="Policy that is attached to a Resizable entity and dynamically "
+        + "adjusts its size in response to either keep a metric within a given range, or in response to "
+        + "POOL_COLD and POOL_HOT events")
+public class AutoScalerPolicy extends AbstractPolicy {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(AutoScalerPolicy.class);
+
+    public static Builder builder() {
+        return new Builder();
+    }
+    
+    public static class Builder {
+        private String id;
+        private String name;
+        private AttributeSensor<? extends Number> metric;
+        private Entity entityWithMetric;
+        private Number metricUpperBound;
+        private Number metricLowerBound;
+        private int minPoolSize = 1;
+        private int maxPoolSize = Integer.MAX_VALUE;
+        private Integer resizeDownIterationIncrement;
+        private Integer resizeDownIterationMax;
+        private Integer resizeUpIterationIncrement;
+        private Integer resizeUpIterationMax;
+        private Duration minPeriodBetweenExecs;
+        private Duration resizeUpStabilizationDelay;
+        private Duration resizeDownStabilizationDelay;
+        private ResizeOperator resizeOperator;
+        private Function<Entity,Integer> currentSizeOperator;
+        private BasicNotificationSensor<?> poolHotSensor;
+        private BasicNotificationSensor<?> poolColdSensor;
+        private BasicNotificationSensor<?> poolOkSensor;
+        private BasicNotificationSensor<? super MaxPoolSizeReachedEvent> maxSizeReachedSensor;
+        private Duration maxReachedNotificationDelay;
+        
+        public Builder id(String val) {
+            this.id = val; return this;
+        }
+        public Builder name(String val) {
+            this.name = val; return this;
+        }
+        public Builder metric(AttributeSensor<? extends Number> val) {
+            this.metric = val; return this;
+        }
+        public Builder entityWithMetric(Entity val) {
+            this.entityWithMetric = val; return this;
+        }
+        public Builder metricLowerBound(Number val) {
+            this.metricLowerBound = val; return this;
+        }
+        public Builder metricUpperBound(Number val) {
+            this.metricUpperBound = val; return this;
+        }
+        public Builder metricRange(Number min, Number max) {
+            metricLowerBound = checkNotNull(min);
+            metricUpperBound = checkNotNull(max);
+            return this;
+        }
+        public Builder minPoolSize(int val) {
+            this.minPoolSize = val; return this;
+        }
+        public Builder maxPoolSize(int val) {
+            this.maxPoolSize = val; return this;
+        }
+        public Builder sizeRange(int min, int max) {
+            minPoolSize = min;
+            maxPoolSize = max;
+            return this;
+        }
+        
+        public Builder resizeUpIterationIncrement(Integer val) {
+            this.resizeUpIterationIncrement = val; return this;
+        }
+        public Builder resizeUpIterationMax(Integer val) {
+            this.resizeUpIterationMax = val; return this;
+        }
+        public Builder resizeDownIterationIncrement(Integer val) {
+            this.resizeUpIterationIncrement = val; return this;
+        }
+        public Builder resizeDownIterationMax(Integer val) {
+            this.resizeUpIterationMax = val; return this;
+        }
+
+        public Builder minPeriodBetweenExecs(Duration val) {
+            this.minPeriodBetweenExecs = val; return this;
+        }
+        public Builder resizeUpStabilizationDelay(Duration val) {
+            this.resizeUpStabilizationDelay = val; return this;
+        }
+        public Builder resizeDownStabilizationDelay(Duration val) {
+            this.resizeDownStabilizationDelay = val; return this;
+        }
+        public Builder resizeOperator(ResizeOperator val) {
+            this.resizeOperator = val; return this;
+        }
+        public Builder currentSizeOperator(Function<Entity, Integer> val) {
+            this.currentSizeOperator = val; return this;
+        }
+        public Builder poolHotSensor(BasicNotificationSensor<?> val) {
+            this.poolHotSensor = val; return this;
+        }
+        public Builder poolColdSensor(BasicNotificationSensor<?> val) {
+            this.poolColdSensor = val; return this;
+        }
+        public Builder poolOkSensor(BasicNotificationSensor<?> val) {
+            this.poolOkSensor = val; return this;
+        }
+        public Builder maxSizeReachedSensor(BasicNotificationSensor<? super MaxPoolSizeReachedEvent> val) {
+            this.maxSizeReachedSensor = val; return this;
+        }
+        public Builder maxReachedNotificationDelay(Duration val) {
+            this.maxReachedNotificationDelay = val; return this;
+        }
+        public AutoScalerPolicy build() {
+            return new AutoScalerPolicy(toFlags());
+        }
+        public PolicySpec<AutoScalerPolicy> buildSpec() {
+            return PolicySpec.create(AutoScalerPolicy.class)
+                    .configure(toFlags());
+        }
+        private Map<String,?> toFlags() {
+            return MutableMap.<String,Object>builder()
+                    .putIfNotNull("id", id)
+                    .putIfNotNull("name", name)
+                    .putIfNotNull("metric", metric)
+                    .putIfNotNull("entityWithMetric", entityWithMetric)
+                    .putIfNotNull("metricUpperBound", metricUpperBound)
+                    .putIfNotNull("metricLowerBound", metricLowerBound)
+                    .putIfNotNull("minPoolSize", minPoolSize)
+                    .putIfNotNull("maxPoolSize", maxPoolSize)
+                    .putIfNotNull("resizeUpIterationMax", resizeUpIterationMax)
+                    .putIfNotNull("resizeUpIterationIncrement", resizeUpIterationIncrement)
+                    .putIfNotNull("resizeDownIterationMax", resizeDownIterationMax)
+                    .putIfNotNull("resizeDownIterationIncrement", resizeDownIterationIncrement)
+                    .putIfNotNull("minPeriodBetweenExecs", minPeriodBetweenExecs)
+                    .putIfNotNull("resizeUpStabilizationDelay", resizeUpStabilizationDelay)
+                    .putIfNotNull("resizeDownStabilizationDelay", resizeDownStabilizationDelay)
+                    .putIfNotNull("resizeOperator", resizeOperator)
+                    .putIfNotNull("currentSizeOperator", currentSizeOperator)
+                    .putIfNotNull("poolHotSensor", poolHotSensor)
+                    .putIfNotNull("poolColdSensor", poolColdSensor)
+                    .putIfNotNull("poolOkSensor", poolOkSensor)
+                    .putIfNotNull("maxSizeReachedSensor", maxSizeReachedSensor)
+                    .putIfNotNull("maxReachedNotificationDelay", maxReachedNotificationDelay)
+                    .build();
+        }
+    }
+    
+    // TODO Is there a nicer pattern for registering such type-coercions? 
+    // Can't put it in the ResizeOperator interface, nor in core TypeCoercions class because interface is defined in policy/.
+    static {
+        TypeCoercions.registerAdapter(Closure.class, ResizeOperator.class, new Function<Closure,ResizeOperator>() {
+            @Override
+            public ResizeOperator apply(final Closure closure) {
+                return new ResizeOperator() {
+                    @Override public Integer resize(Entity entity, Integer input) {
+                        return (Integer) closure.call(entity, input);
+                    }
+                };
+            }
+        });
+    }
+    
+    // Pool workrate notifications.
+    public static BasicNotificationSensor<Map> DEFAULT_POOL_HOT_SENSOR = new BasicNotificationSensor<Map>(
+        Map.class, "resizablepool.hot", "Pool is over-utilized; it has insufficient resource for current workload");
+    public static BasicNotificationSensor<Map> DEFAULT_POOL_COLD_SENSOR = new BasicNotificationSensor<Map>(
+        Map.class, "resizablepool.cold", "Pool is under-utilized; it has too much resource for current workload");
+    public static BasicNotificationSensor<Map> DEFAULT_POOL_OK_SENSOR = new BasicNotificationSensor<Map>(
+        Map.class, "resizablepool.cold", "Pool utilization is ok; the available resources are fine for the current workload");
+
+    /**
+     * A convenience for policies that want to register a {@code builder.maxSizeReachedSensor(sensor)}.
+     * Note that this "default" is not set automatically; the default is for no sensor to be used (so
+     * no events emitted).
+     */
+    public static BasicNotificationSensor<MaxPoolSizeReachedEvent> DEFAULT_MAX_SIZE_REACHED_SENSOR = new BasicNotificationSensor<MaxPoolSizeReachedEvent>(
+            MaxPoolSizeReachedEvent.class, "resizablepool.maxSizeReached", "Consistently wanted to resize the pool above the max allowed size");
+
+    public static final String POOL_CURRENT_SIZE_KEY = "pool.current.size";
+    public static final String POOL_HIGH_THRESHOLD_KEY = "pool.high.threshold";
+    public static final String POOL_LOW_THRESHOLD_KEY = "pool.low.threshold";
+    public static final String POOL_CURRENT_WORKRATE_KEY = "pool.current.workrate";
+    
+    @SuppressWarnings("serial")
+    @SetFromFlag("metric")
+    public static final ConfigKey<AttributeSensor<? extends Number>> METRIC = BasicConfigKey.builder(new TypeToken<AttributeSensor<? extends Number>>() {})
+            .name("autoscaler.metric")
+            .build();
+
+    @SetFromFlag("entityWithMetric")
+    public static final ConfigKey<Entity> ENTITY_WITH_METRIC = BasicConfigKey.builder(Entity.class)
+            .name("autoscaler.entityWithMetric")
+            .build();
+    
+    @SetFromFlag("metricLowerBound")
+    public static final ConfigKey<Number> METRIC_LOWER_BOUND = BasicConfigKey.builder(Number.class)
+            .name("autoscaler.metricLowerBound")
+            .reconfigurable(true)
+            .build();
+    
+    @SetFromFlag("metricUpperBound")
+    public static final ConfigKey<Number> METRIC_UPPER_BOUND = BasicConfigKey.builder(Number.class)
+            .name("autoscaler.metricUpperBound")
+            .reconfigurable(true)
+            .build();
+    
+    @SetFromFlag("resizeUpIterationIncrement")
+    public static final ConfigKey<Integer> RESIZE_UP_ITERATION_INCREMENT = BasicConfigKey.builder(Integer.class)
+            .name("autoscaler.resizeUpIterationIncrement")
+            .description("Batch size for resizing up; the size will be increased by a multiple of this value")
+            .defaultValue(1)
+            .reconfigurable(true)
+            .build();
+    @SetFromFlag("resizeUpIterationMax")
+    public static final ConfigKey<Integer> RESIZE_UP_ITERATION_MAX = BasicConfigKey.builder(Integer.class)
+            .name("autoscaler.resizeUpIterationMax")
+            .defaultValue(Integer.MAX_VALUE)
+            .description("Maximum change to the size on a single iteration when scaling up")
+            .reconfigurable(true)
+            .build();
+    @SetFromFlag("resizeDownIterationIncrement")
+    public static final ConfigKey<Integer> RESIZE_DOWN_ITERATION_INCREMENT = BasicConfigKey.builder(Integer.class)
+            .name("autoscaler.resizeDownIterationIncrement")
+            .description("Batch size for resizing down; the size will be decreased by a multiple of this value")
+            .defaultValue(1)
+            .reconfigurable(true)
+            .build();
+    @SetFromFlag("resizeDownIterationMax")
+    public static final ConfigKey<Integer> RESIZE_DOWN_ITERATION_MAX = BasicConfigKey.builder(Integer.class)
+            .name("autoscaler.resizeDownIterationMax")
+            .defaultValue(Integer.MAX_VALUE)
+            .description("Maximum change to the size on a single iteration when scaling down")
+            .reconfigurable(true)
+            .build();
+
+    @SetFromFlag("minPeriodBetweenExecs")
+    public static final ConfigKey<Duration> MIN_PERIOD_BETWEEN_EXECS = BasicConfigKey.builder(Duration.class)
+            .name("autoscaler.minPeriodBetweenExecs")
+            .defaultValue(Duration.millis(100))
+            .build();
+    
+    @SetFromFlag("resizeUpStabilizationDelay")
+    public static final ConfigKey<Duration> RESIZE_UP_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
+            .name("autoscaler.resizeUpStabilizationDelay")
+            .defaultValue(Duration.ZERO)
+            .reconfigurable(true)
+            .build();
+    
+    @SetFromFlag("resizeDownStabilizationDelay")
+    public static final ConfigKey<Duration> RESIZE_DOWN_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
+            .name("autoscaler.resizeDownStabilizationDelay")
+            .defaultValue(Duration.ZERO)
+            .reconfigurable(true)
+            .build();
+
+    @SetFromFlag("minPoolSize")
+    public static final ConfigKey<Integer> MIN_POOL_SIZE = BasicConfigKey.builder(Integer.class)
+            .name("autoscaler.minPoolSize")
+            .defaultValue(1)
+            .reconfigurable(true)
+            .build();
+    
+    @SetFromFlag("maxPoolSize")
+    public static final ConfigKey<Integer> MAX_POOL_SIZE = BasicConfigKey.builder(Integer.class)
+            .name("autoscaler.maxPoolSize")
+            .defaultValue(Integer.MAX_VALUE)
+            .reconfigurable(true)
+            .build();
+
+    @SetFromFlag("resizeOperator")
+    public static final ConfigKey<ResizeOperator> RESIZE_OPERATOR = BasicConfigKey.builder(ResizeOperator.class)
+            .name("autoscaler.resizeOperator")
+            .defaultValue(new ResizeOperator() {
+                    public Integer resize(Entity entity, Integer desiredSize) {
+                        return ((Resizable)entity).resize(desiredSize);
+                    }})
+            .build();
+    
+    @SuppressWarnings("serial")
+    @SetFromFlag("currentSizeOperator")
+    public static final ConfigKey<Function<Entity,Integer>> CURRENT_SIZE_OPERATOR = BasicConfigKey.builder(new TypeToken<Function<Entity,Integer>>() {})
+            .name("autoscaler.currentSizeOperator")
+            .defaultValue(new Function<Entity,Integer>() {
+                    public Integer apply(Entity entity) {
+                        return ((Resizable)entity).getCurrentSize();
+                    }})
+            .build();
+
+    @SuppressWarnings("serial")
+    @SetFromFlag("poolHotSensor")
+    public static final ConfigKey<BasicNotificationSensor<? extends Map>> POOL_HOT_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? extends Map>>() {})
+            .name("autoscaler.poolHotSensor")
+            .defaultValue(DEFAULT_POOL_HOT_SENSOR)
+            .build();
+
+    @SuppressWarnings("serial")
+    @SetFromFlag("poolColdSensor")
+    public static final ConfigKey<BasicNotificationSensor<? extends Map>> POOL_COLD_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? extends Map>>() {})
+            .name("autoscaler.poolColdSensor")
+            .defaultValue(DEFAULT_POOL_COLD_SENSOR)
+            .build();
+
+    @SuppressWarnings("serial")
+    @SetFromFlag("poolOkSensor")
+    public static final ConfigKey<BasicNotificationSensor<? extends Map>> POOL_OK_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? extends Map>>() {})
+            .name("autoscaler.poolOkSensor")
+            .defaultValue(DEFAULT_POOL_OK_SENSOR)
+            .build();
+
+    @SuppressWarnings("serial")
+    @SetFromFlag("maxSizeReachedSensor")
+    public static final ConfigKey<BasicNotificationSensor<? super MaxPoolSizeReachedEvent>> MAX_SIZE_REACHED_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? super MaxPoolSizeReachedEvent>>() {})
+            .name("autoscaler.maxSizeReachedSensor")
+            .description("Sensor for which a notification will be emitted (on the associated entity) when " +
+                    "we consistently wanted to resize the pool above the max allowed size, for " +
+                    "maxReachedNotificationDelay milliseconds")
+            .build();
+    
+    @SetFromFlag("maxReachedNotificationDelay")
+    public static final ConfigKey<Duration> MAX_REACHED_NOTIFICATION_DELAY = BasicConfigKey.builder(Duration.class)
+            .name("autoscaler.maxReachedNotificationDelay")
+            .description("Time that we consistently wanted to go above the maxPoolSize for, after which the " +
+                    "maxSizeReachedSensor (if any) will be emitted")
+            .defaultValue(Duration.ZERO)
+            .build();
+    
+    private Entity poolEntity;
+    
+    private final AtomicBoolean executorQueued = new AtomicBoolean(false);
+    private volatile long executorTime = 0;
+    private volatile ScheduledExecutorService executor;
+
+    private SizeHistory recentUnboundedResizes;
+
+    private SizeHistory recentDesiredResizes;
+    
+    private long maxReachedLastNotifiedTime;
+    
+    private final SensorEventListener<Map> utilizationEventHandler = new SensorEventListener<Map>() {
+        public void onEvent(SensorEvent<Map> event) {
+            Map<String, ?> properties = (Map<String, ?>) event.getValue();
+            Sensor<?> sensor = event.getSensor();
+            
+            if (sensor.equals(getPoolColdSensor())) {
+                onPoolCold(properties);
+            } else if (sensor.equals(getPoolHotSensor())) {
+                onPoolHot(properties);
+            } else if (sensor.equals(getPoolOkSensor())) {
+                onPoolOk(properties);
+            } else {
+                throw new IllegalStateException("Unexpected sensor type: "+sensor+"; event="+event);
+            }
+        }
+    };
+
+    private final SensorEventListener<Number> metricEventHandler = new SensorEventListener<Number>() {
+        public void onEvent(SensorEvent<Number> event) {
+            assert event.getSensor().equals(getMetric());
+            onMetricChanged(event.getValue());
+        }
+    };
+
+    public AutoScalerPolicy() {
+        this(MutableMap.<String,Object>of());
+    }
+    
+    public AutoScalerPolicy(Map<String,?> props) {
+        super(props);
+    }
+
+    @Override
+    public void init() {
+        doInit();
+    }
+
+    @Override
+    public void rebind() {
+        doInit();
+    }
+    
+    protected void doInit() {
+        long maxReachedNotificationDelay = getMaxReachedNotificationDelay().toMilliseconds();
+        recentUnboundedResizes = new SizeHistory(maxReachedNotificationDelay);
+        
+        long maxResizeStabilizationDelay = Math.max(getResizeUpStabilizationDelay().toMilliseconds(), getResizeDownStabilizationDelay().toMilliseconds());
+        recentDesiredResizes = new SizeHistory(maxResizeStabilizationDelay);
+        
+        // TODO Should re-use the execution manager's thread pool, somehow
+        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
+    }
+
+    public void setMetricLowerBound(Number val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing metricLowerBound from {} to {}", new Object[] {this, getMetricLowerBound(), val});
+        config().set(METRIC_LOWER_BOUND, checkNotNull(val));
+    }
+    
+    public void setMetricUpperBound(Number val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing metricUpperBound from {} to {}", new Object[] {this, getMetricUpperBound(), val});
+        config().set(METRIC_UPPER_BOUND, checkNotNull(val));
+    }
+    
+    private <T> void setOrDefault(ConfigKey<T> key, T val) {
+        if (val==null) val = key.getDefaultValue();
+        config().set(key, val);
+    }
+    public int getResizeUpIterationIncrement() { return getConfig(RESIZE_UP_ITERATION_INCREMENT); }
+    public void setResizeUpIterationIncrement(Integer val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeUpIterationIncrement from {} to {}", new Object[] {this, getResizeUpIterationIncrement(), val});
+        setOrDefault(RESIZE_UP_ITERATION_INCREMENT, val);
+    }
+    public int getResizeDownIterationIncrement() { return getConfig(RESIZE_DOWN_ITERATION_INCREMENT); }
+    public void setResizeDownIterationIncrement(Integer val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeDownIterationIncrement from {} to {}", new Object[] {this, getResizeDownIterationIncrement(), val});
+        setOrDefault(RESIZE_DOWN_ITERATION_INCREMENT, val);
+    }
+    public int getResizeUpIterationMax() { return getConfig(RESIZE_UP_ITERATION_MAX); }
+    public void setResizeUpIterationMax(Integer val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeUpIterationMax from {} to {}", new Object[] {this, getResizeUpIterationMax(), val});
+        setOrDefault(RESIZE_UP_ITERATION_MAX, val);
+    }
+    public int getResizeDownIterationMax() { return getConfig(RESIZE_DOWN_ITERATION_MAX); }
+    public void setResizeDownIterationMax(Integer val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeDownIterationMax from {} to {}", new Object[] {this, getResizeDownIterationMax(), val});
+        setOrDefault(RESIZE_DOWN_ITERATION_MAX, val);
+    }
+
+    public void setMinPeriodBetweenExecs(Duration val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing minPeriodBetweenExecs from {} to {}", new Object[] {this, getMinPeriodBetweenExecs(), val});
+        config().set(MIN_PERIOD_BETWEEN_EXECS, val);
+    }
+
+    public void setResizeUpStabilizationDelay(Duration val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeUpStabilizationDelay from {} to {}", new Object[] {this, getResizeUpStabilizationDelay(), val});
+        config().set(RESIZE_UP_STABILIZATION_DELAY, val);
+    }
+    
+    public void setResizeDownStabilizationDelay(Duration val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeDownStabilizationDelay from {} to {}", new Object[] {this, getResizeDownStabilizationDelay(), val});
+        config().set(RESIZE_DOWN_STABILIZATION_DELAY, val);
+    }
+    
+    public void setMinPoolSize(int val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing minPoolSize from {} to {}", new Object[] {this, getMinPoolSize(), val});
+        config().set(MIN_POOL_SIZE, val);
+    }
+    
+    public void setMaxPoolSize(int val) {
+        if (LOG.isInfoEnabled()) LOG.info("{} changing maxPoolSize from {} to {}", new Object[] {this, getMaxPoolSize(), val});
+        config().set(MAX_POOL_SIZE, val);
+    }
+    
+    private AttributeSensor<? extends Number> getMetric() {
+        return getConfig(METRIC);
+    }
+
+    private Entity getEntityWithMetric() {
+        return getConfig(ENTITY_WITH_METRIC);
+    }
+    
+    private Number getMetricLowerBound() {
+        return getConfig(METRIC_LOWER_BOUND);
+    }
+    
+    private Number getMetricUpperBound() {
+        return getConfig(METRIC_UPPER_BOUND);
+    }
+    
+    private Duration getMinPeriodBetweenExecs() {
+        return getConfig(MIN_PERIOD_BETWEEN_EXECS);
+    }
+    
+    private Duration getResizeUpStabilizationDelay() {
+        return getConfig(RESIZE_UP_STABILIZATION_DELAY);
+    }
+    
+    private Duration getResizeDownStabilizationDelay() {
+        return getConfig(RESIZE_DOWN_STABILIZATION_DELAY);
+    }
+    
+    private int getMinPoolSize() {
+        return getConfig(MIN_POOL_SIZE);
+    }
+    
+    private int getMaxPoolSize() {
+        return getConfig(MAX_POOL_SIZE);
+    }
+    
+    private ResizeOperator getResizeOperator() {
+        return getConfig(RESIZE_OPERATOR);
+    }
+    
+    private Function<Entity,Integer> getCurrentSizeOperator() {
+        return getConfig(CURRENT_SIZE_OPERATOR);
+    }
+    
+    private BasicNotificationSensor<? extends Map> getPoolHotSensor() {
+        return getConfig(POOL_HOT_SENSOR);
+    }
+    
+    private BasicNotificationSensor<? extends Map> getPoolColdSensor() {
+        return getConfig(POOL_COLD_SENSOR);
+    }
+    
+    private BasicNotificationSensor<? extends Map> getPoolOkSensor() {
+        return getConfig(POOL_OK_SENSOR);
+    }
+    
+    private BasicNotificationSensor<? super MaxPoolSizeReachedEvent> getMaxSizeReachedSensor() {
+        return getConfig(MAX_SIZE_REACHED_SENSOR);
+    }
+    
+    private Duration getMaxReachedNotificationDelay() {
+        return getConfig(MAX_REACHED_NOTIFICATION_DELAY);
+    }
+
+    @Override
+    protected <T> void doReconfigureConfig(ConfigKey<T> key, T val) {
+        if (key.equals(RESIZE_UP_STABILIZATION_DELAY)) {
+            Duration maxResizeStabilizationDelay = Duration.max((Duration)val, getResizeDownStabilizationDelay());
+            recentDesiredResizes.setWindowSize(maxResizeStabilizationDelay);
+        } else if (key.equals(RESIZE_DOWN_STABILIZATION_DELAY)) {
+            Duration maxResizeStabilizationDelay = Duration.max((Duration)val, getResizeUpStabilizationDelay());
+            recentDesiredResizes.setWindowSize(maxResizeStabilizationDelay);
+        } else if (key.equals(METRIC_LOWER_BOUND)) {
+            // TODO If recorded what last metric value was then we could recalculate immediately
+            // Rely on next metric-change to trigger recalculation; 
+            // and same for those below...
+        } else if (key.equals(METRIC_UPPER_BOUND)) {
+            // see above
+        } else if (key.equals(RESIZE_UP_ITERATION_INCREMENT) || key.equals(RESIZE_UP_ITERATION_MAX) || key.equals(RESIZE_DOWN_ITERATION_INCREMENT) || key.equals(RESIZE_DOWN_ITERATION_MAX)) {
+            // no special actions needed
+        } else if (key.equals(MIN_POOL_SIZE)) {
+            int newMin = (Integer) val;
+            if (newMin > getConfig(MAX_POOL_SIZE)) {
+                throw new IllegalArgumentException("Min pool size "+val+" must not be greater than max pool size "+getConfig(MAX_POOL_SIZE));
+            }
+            onPoolSizeLimitsChanged(newMin, getConfig(MAX_POOL_SIZE));
+        } else if (key.equals(MAX_POOL_SIZE)) {
+            int newMax = (Integer) val;
+            if (newMax < getConfig(MIN_POOL_SIZE)) {
+                throw new IllegalArgumentException("Min pool size "+val+" must not be greater than max pool size "+getConfig(MAX_POOL_SIZE));
+            }
+            onPoolSizeLimitsChanged(getConfig(MIN_POOL_SIZE), newMax);
+        } else {
+            throw new UnsupportedOperationException("reconfiguring "+key+" unsupported for "+this);
+        }
+    }
+
+    @Override
+    public void suspend() {
+        super.suspend();
+        // TODO unsubscribe from everything? And resubscribe on resume?
+        if (executor != null) executor.shutdownNow();
+    }
+    
+    @Override
+    public void resume() {
+        super.resume();
+        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
+    }
+    
+    @Override
+    public void setEntity(EntityLocal entity) {
+        if (!config().getRaw(RESIZE_OPERATOR).isPresentAndNonNull()) {
+            Preconditions.checkArgument(entity instanceof Resizable, "Provided entity "+entity+" must be an instance of Resizable, because no custom-resizer operator supplied");
+        }
+        super.setEntity(entity);
+        this.poolEntity = entity;
+        
+        if (getMetric() != null) {
+            Entity entityToSubscribeTo = (getEntityWithMetric() != null) ? getEntityWithMetric() : entity;
+            subscribe(entityToSubscribeTo, getMetric(), metricEventHandler);
+        }
+        subscribe(poolEntity, getPoolColdSensor(), utilizationEventHandler);
+        subscribe(poolEntity, getPoolHotSensor(), utilizationEventHandler);
+        subscribe(poolEntity, getPoolOkSensor(), utilizationEventHandler);
+    }
+    
+    private ThreadFactory newThreadFactory() {
+        return new ThreadFactoryBuilder()
+                .setNameFormat("brooklyn-autoscalerpolicy-%d")
+                .build();
+    }
+
+    /**
+     * Forces an immediate resize (without waiting for stabilization etc) if the current size is 
+     * not within the min and max limits. We schedule this so that all resize operations are done
+     * by the same thread.
+     */
+    private void onPoolSizeLimitsChanged(final int min, final int max) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} checking pool size on limits changed for {} (between {} and {})", new Object[] {this, poolEntity, min, max});
+        
+        if (isRunning() && isEntityUp()) {
+            executor.submit(new Runnable() {
+                @Override public void run() {
+                    try {
+                        int currentSize = getCurrentSizeOperator().apply(entity);
+                        int desiredSize = Math.min(max, Math.max(min, currentSize));
+
+                        if (currentSize != desiredSize) {
+                            if (LOG.isInfoEnabled()) LOG.info("{} resizing pool {} immediateley from {} to {} (due to new pool size limits)", new Object[] {this, poolEntity, currentSize, desiredSize});
+                            getResizeOperator().resize(poolEntity, desiredSize);
+                        }
+                        
+                    } catch (Exception e) {
+                        if (isRunning()) {
+                            LOG.error("Error resizing: "+e, e);
+                        } else {
+                            if (LOG.isDebugEnabled()) LOG.debug("Error resizing, but no longer running: "+e, e);
+                        }
+                    } catch (Throwable t) {
+                        LOG.error("Error resizing: "+t, t);
+                        throw Throwables.propagate(t);
+                    }
+                }});
+        }
+    }
+    
+    private enum ScalingType { HOT, COLD }
+    private static class ScalingData {
+        ScalingType scalingMode;
+        int currentSize;
+        double currentMetricValue;
+        Double metricUpperBound;
+        Double metricLowerBound;
+        
+        public double getCurrentTotalActivity() {
+            return currentMetricValue * currentSize;
+        }
+        
+        public boolean isHot() {
+            return ((scalingMode==null || scalingMode==ScalingType.HOT) && isValid(metricUpperBound) && currentMetricValue > metricUpperBound);
+        }
+        public boolean isCold() {
+            return ((scalingMode==null || scalingMode==ScalingType.COLD) && isValid(metricLowerBound) && currentMetricValue < metricLowerBound);
+        }
+        private boolean isValid(Double bound) {
+            return (bound!=null && bound>0);
+        }
+    }
+
+    private void onMetricChanged(Number val) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-metric for {}: {}", new Object[] {this, poolEntity, val});
+
+        if (val==null) {
+            // occurs e.g. if using an aggregating enricher who returns null when empty, the sensor has gone away
+            if (LOG.isTraceEnabled()) LOG.trace("{} not resizing pool {}, inbound metric is null", new Object[] {this, poolEntity});
+            return;
+        }
+        
+        ScalingData data = new ScalingData();
+        data.currentMetricValue = val.doubleValue();
+        data.currentSize = getCurrentSizeOperator().apply(entity);
+        data.metricUpperBound = getMetricUpperBound().doubleValue();
+        data.metricLowerBound = getMetricLowerBound().doubleValue();
+        
+        analyze(data, "pool");
+    }
+    
+    private void onPoolCold(Map<String, ?> properties) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-cold for {}: {}", new Object[] {this, poolEntity, properties});
+        analyzeOnHotOrColdSensor(ScalingType.COLD, "cold pool", properties);
+    }
+    
+    private void onPoolHot(Map<String, ?> properties) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-hot for {}: {}", new Object[] {this, poolEntity, properties});
+        analyzeOnHotOrColdSensor(ScalingType.HOT, "hot pool", properties);
+    }
+    
+    private void analyzeOnHotOrColdSensor(ScalingType scalingMode, String description, Map<String, ?> properties) {
+        ScalingData data = new ScalingData();
+        data.scalingMode = scalingMode;
+        data.currentMetricValue = (Double) properties.get(POOL_CURRENT_WORKRATE_KEY);
+        data.currentSize = (Integer) properties.get(POOL_CURRENT_SIZE_KEY);
+        data.metricUpperBound = (Double) properties.get(POOL_HIGH_THRESHOLD_KEY);
+        data.metricLowerBound = (Double) properties.get(POOL_LOW_THRESHOLD_KEY);
+        
+        analyze(data, description);   
+    }
+    
+    private void analyze(ScalingData data, String description) {
+        int desiredSizeUnconstrained;
+        
+        /* We always scale out (modulo stabilization delay) if:
+         *   currentTotalActivity > currentSize*metricUpperBound
+         * With newDesiredSize the smallest n such that   n*metricUpperBound >= currentTotalActivity
+         * ie  n >= currentTotalActiviy/metricUpperBound, thus n := Math.ceil(currentTotalActivity/metricUpperBound)
+         * 
+         * Else consider scale back if:
+         *   currentTotalActivity < currentSize*metricLowerBound
+         * With newDesiredSize normally the largest n such that:  
+         *   n*metricLowerBound <= currentTotalActivity
+         * BUT with an absolute requirement which trumps the above computation
+         * that the newDesiredSize doesn't cause immediate scale out:
+         *   n*metricUpperBound >= currentTotalActivity
+         * thus n := Math.max ( floor(currentTotalActiviy/metricLowerBound), ceil(currentTotal/metricUpperBound) )
+         */
+        if (data.isHot()) {
+            // scale out
+            desiredSizeUnconstrained = (int)Math.ceil(data.getCurrentTotalActivity() / data.metricUpperBound);
+            data.scalingMode = ScalingType.HOT;
+            
+        } else if (data.isCold()) {
+            // scale back
+            desiredSizeUnconstrained = (int)Math.floor(data.getCurrentTotalActivity() / data.metricLowerBound);
+            data.scalingMode = ScalingType.COLD;
+            
+        } else {
+            if (LOG.isTraceEnabled()) LOG.trace("{} not resizing pool {} from {} ({} within range {}..{})", new Object[] {this, poolEntity, data.currentSize, data.currentMetricValue, data.metricLowerBound, data.metricUpperBound});
+            abortResize(data.currentSize);
+            return; // within the healthy range; no-op
+        }
+        
+        if (LOG.isTraceEnabled()) LOG.debug("{} detected unconstrained desired size {}", new Object[] {this, desiredSizeUnconstrained});
+        int desiredSize = applyMinMaxConstraints(desiredSizeUnconstrained);
+
+        if ((data.scalingMode==ScalingType.COLD) && (desiredSize < data.currentSize)) {
+
+            int delta = data.currentSize - desiredSize;
+            int scaleIncrement = getResizeDownIterationIncrement();
+            int scaleMax = getResizeDownIterationMax();
+            if (delta>scaleMax) {
+                delta=scaleMax;
+            } else if (delta % scaleIncrement != 0) {
+                // keep scaling to the increment
+                delta += scaleIncrement - (delta % scaleIncrement);
+            }
+            desiredSize = data.currentSize - delta;
+            
+            if (data.metricUpperBound!=null) {
+                // if upper bound supplied, check that this desired scale-back size 
+                // is not going to cause scale-out on next run; i.e. anti-thrashing
+                while (desiredSize < data.currentSize && data.getCurrentTotalActivity() > data.metricUpperBound * desiredSize) {
+                    if (LOG.isTraceEnabled()) LOG.trace("{} when resizing back pool {} from {}, tweaking from {} to prevent thrashing", new Object[] {this, poolEntity, data.currentSize, desiredSize });
+                    desiredSize += scaleIncrement;
+                }
+            }
+            desiredSize = applyMinMaxConstraints(desiredSize);
+            if (desiredSize >= data.currentSize) data.scalingMode = null;
+            
+        } else if ((data.scalingMode==ScalingType.HOT) && (desiredSize > data.currentSize)) {
+
+            int delta = desiredSize - data.currentSize;
+            int scaleIncrement = getResizeUpIterationIncrement();
+            int scaleMax = getResizeUpIterationMax();
+            if (delta>scaleMax) {
+                delta=scaleMax;
+            } else if (delta % scaleIncrement != 0) {
+                // keep scaling to the increment
+                delta += scaleIncrement - (delta % scaleIncrement);
+            }
+            desiredSize = data.currentSize + delta;
+            desiredSize = applyMinMaxConstraints(desiredSize);
+            if (desiredSize <= data.currentSize) data.scalingMode = null;
+
+        } else {
+            data.scalingMode = null;
+        }
+    
+        if (data.scalingMode!=null) {
+            if (LOG.isDebugEnabled()) LOG.debug("{} provisionally resizing {} {} from {} to {} ({} < {}; ideal size {})", new Object[] {this, description, poolEntity, data.currentSize, desiredSize, data.currentMetricValue, data.metricLowerBound, desiredSizeUnconstrained});
+            scheduleResize(desiredSize);
+        } else {
+            if (LOG.isTraceEnabled()) LOG.trace("{} not resizing {} {} from {} to {}, {} out of healthy range {}..{} but unconstrained size {} blocked by bounds/check", new Object[] {this, description, poolEntity, data.currentSize, desiredSize, data.currentMetricValue, data.metricLowerBound, data.metricUpperBound, desiredSizeUnconstrained});
+            abortResize(data.currentSize);
+            // but add to the unbounded record for future consideration
+        }
+        
+        onNewUnboundedPoolSize(desiredSizeUnconstrained);
+    }
+
+    private int applyMinMaxConstraints(int desiredSize) {
+        desiredSize = Math.max(getMinPoolSize(), desiredSize);
+        desiredSize = Math.min(getMaxPoolSize(), desiredSize);
+        return desiredSize;
+    }
+
+    private void onPoolOk(Map<String, ?> properties) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-ok for {}: {}", new Object[] {this, poolEntity, properties});
+        
+        int poolCurrentSize = (Integer) properties.get(POOL_CURRENT_SIZE_KEY);
+        
+        if (LOG.isTraceEnabled()) LOG.trace("{} not resizing ok pool {} from {}", new Object[] {this, poolEntity, poolCurrentSize});
+        abortResize(poolCurrentSize);
+    }
+
+    /**
+     * Schedules a resize, if there is not already a resize operation queued up. When that resize
+     * executes, it will resize to whatever the latest value is to be (rather than what it was told
+     * to do at the point the job was queued).
+     */
+    private void scheduleResize(final int newSize) {
+        recentDesiredResizes.add(newSize);
+        
+        scheduleResize();
+    }
+
+    /**
+     * If a listener is registered to be notified of the max-pool-size cap being reached, then record
+     * what our unbounded size would be and schedule a check to see if this unbounded size is sustained.
+     * 
+     * Piggy-backs off the existing scheduleResize execution, which now also checks if the listener
+     * needs to be called.
+     */
+    private void onNewUnboundedPoolSize(final int val) {
+        if (getMaxSizeReachedSensor() != null) {
+            recentUnboundedResizes.add(val);
+            scheduleResize();
+        }
+    }
+    
+    private void abortResize(final int currentSize) {
+        recentDesiredResizes.add(currentSize);
+        recentUnboundedResizes.add(currentSize);
+    }
+
+    private boolean isEntityUp() {
+        if (entity == null) {
+            return false;
+        } else if (entity.getEntityType().getSensors().contains(Startable.SERVICE_UP)) {
+            return Boolean.TRUE.equals(entity.getAttribute(Startable.SERVICE_UP));
+        } else {
+            return true;
+        }
+    }
+
+    private void scheduleResize() {
+        // TODO Make scale-out calls concurrent, rather than waiting for first resize to entirely 
+        // finish. On ec2 for example, this can cause us to grow very slowly if first request is for
+        // just one new VM to be provisioned.
+        
+        if (isRunning() && isEntityUp() && executorQueued.compareAndSet(false, true)) {
+            long now = System.currentTimeMillis();
+            long delay = Math.max(0, (executorTime + getMinPeriodBetweenExecs().toMilliseconds()) - now);
+            if (LOG.isTraceEnabled()) LOG.trace("{} scheduling resize in {}ms", this, delay);
+            
+            executor.schedule(new Runnable() {
+                @Override public void run() {
+                    try {
+                        executorTime = System.currentTimeMillis();
+                        executorQueued.set(false);
+
+                        resizeNow();
+                        notifyMaxReachedIfRequiredNow();
+                        
+                    } catch (Exception e) {
+                        if (isRunning()) {
+                            LOG.error("Error resizing: "+e, e);
+                        } else {
+                            if (LOG.isDebugEnabled()) LOG.debug("Error resizing, but no longer running: "+e, e);
+                        }
+                    } catch (Throwable t) {
+                        LOG.error("Error resizing: "+t, t);
+                        throw Throwables.propagate(t);
+                    }
+                }},
+                delay,
+                TimeUnit.MILLISECONDS);
+        }
+    }
+
+    /**
+     * Looks at the values for "unbounded pool size" (i.e. if we ignore caps of minSize and maxSize) to report what
+     * those values have been within a time window. The time window used is the "maxReachedNotificationDelay",
+     * which determines how many milliseconds after being consistently above the max-size will it take before
+     * we emit the sensor event (if any).
+     */
+    private void notifyMaxReachedIfRequiredNow() {
+        BasicNotificationSensor<? super MaxPoolSizeReachedEvent> maxSizeReachedSensor = getMaxSizeReachedSensor();
+        if (maxSizeReachedSensor == null) {
+            return;
+        }
+        
+        WindowSummary valsSummary = recentUnboundedResizes.summarizeWindow(getMaxReachedNotificationDelay());
+        long timeWindowSize = getMaxReachedNotificationDelay().toMilliseconds();
+        long currentPoolSize = getCurrentSizeOperator().apply(poolEntity);
+        int maxAllowedPoolSize = getMaxPoolSize();
+        long unboundedSustainedMaxPoolSize = valsSummary.min; // The sustained maximum (i.e. the smallest it's dropped down to)
+        long unboundedCurrentPoolSize = valsSummary.latest;
+        
+        if (maxReachedLastNotifiedTime > 0) {
+            // already notified the listener; don't do it again
+            // TODO Could have max period for notifications, or a step increment to warn when exceeded by ever bigger amounts
+            
+        } else if (unboundedSustainedMaxPoolSize > maxAllowedPoolSize) {
+            // We have consistently wanted to be bigger than the max allowed; tell the listener
+            if (LOG.isDebugEnabled()) LOG.debug("{} notifying listener of max pool size reached; current {}, max {}, unbounded current {}, unbounded max {}", 
+                    new Object[] {this, currentPoolSize, maxAllowedPoolSize, unboundedCurrentPoolSize, unboundedSustainedMaxPoolSize});
+            
+            maxReachedLastNotifiedTime = System.currentTimeMillis();
+            MaxPoolSizeReachedEvent event = MaxPoolSizeReachedEvent.builder()
+                    .currentPoolSize(currentPoolSize)
+                    .maxAllowed(maxAllowedPoolSize)
+                    .currentUnbounded(unboundedCurrentPoolSize)
+                    .maxUnbounded(unboundedSustainedMaxPoolSize)
+                    .timeWindow(timeWindowSize)
+                    .build();
+            entity.emit(maxSizeReachedSensor, event);
+            
+        } else if (valsSummary.max > maxAllowedPoolSize) {
+            // We temporarily wanted to be bigger than the max allowed; check back later to see if consistent
+            // TODO Could check if there has been anything bigger than "min" since min happened (would be more efficient)
+            if (LOG.isTraceEnabled()) LOG.trace("{} re-scheduling max-reached check for {}, as unbounded size not stable (min {}, max {}, latest {})", 
+                    new Object[] {this, poolEntity, valsSummary.min, valsSummary.max, valsSummary.latest});
+            scheduleResize();
+            
+        } else {
+            // nothing to write home about; continually below maxAllowed
+        }
+    }
+
+    private void resizeNow() {
+        long currentPoolSize = getCurrentSizeOperator().apply(poolEntity);
+        CalculatedDesiredPoolSize calculatedDesiredPoolSize = calculateDesiredPoolSize(currentPoolSize);
+        final long desiredPoolSize = calculatedDesiredPoolSize.size;
+        boolean stable = calculatedDesiredPoolSize.stable;
+        
+        if (!stable) {
+            // the desired size fluctuations are not stable; ensure we check again later (due to time-window)
+            // even if no additional events have been received
+            // (note we continue now with as "good" a resize as we can given the instability)
+            if (LOG.isTraceEnabled()) LOG.trace("{} re-scheduling resize check for {}, as desired size not stable (current {}, desired {}); continuing with resize...", 
+                    new Object[] {this, poolEntity, currentPoolSize, desiredPoolSize});
+            scheduleResize();
+        }
+        if (currentPoolSize == desiredPoolSize) {
+            if (LOG.isTraceEnabled()) LOG.trace("{} not resizing pool {} from {} to {}", 
+                    new Object[] {this, poolEntity, currentPoolSize, desiredPoolSize});
+            return;
+        }
+        
+        if (LOG.isDebugEnabled()) LOG.debug("{} requesting resize to {}; current {}, min {}, max {}", 
+                new Object[] {this, desiredPoolSize, currentPoolSize, getMinPoolSize(), getMaxPoolSize()});
+        
+        Entities.submit(entity, Tasks.<Void>builder().name("Auto-scaler")
+            .description("Auto-scaler recommending resize from "+currentPoolSize+" to "+desiredPoolSize)
+            .tag(BrooklynTaskTags.NON_TRANSIENT_TASK_TAG)
+            .body(new Callable<Void>() {
+                @Override
+                public Void call() throws Exception {
+                    // TODO Should we use int throughout, rather than casting here?
+                    getResizeOperator().resize(poolEntity, (int) desiredPoolSize);
+                    return null;
+                }
+            }).build())
+            .blockUntilEnded();
+    }
+    
+    /**
+     * Complicated logic for stabilization-delay...
+     * Only grow if we have consistently been asked to grow for the resizeUpStabilizationDelay period;
+     * Only shrink if we have consistently been asked to shrink for the resizeDownStabilizationDelay period.
+     * 
+     * @return tuple of desired pool size, and whether this is "stable" (i.e. if we receive no more events 
+     *         will this continue to be the desired pool size)
+     */
+    private CalculatedDesiredPoolSize calculateDesiredPoolSize(long currentPoolSize) {
+        long now = System.currentTimeMillis();
+        WindowSummary downsizeSummary = recentDesiredResizes.summarizeWindow(getResizeDownStabilizationDelay());
+        WindowSummary upsizeSummary = recentDesiredResizes.summarizeWindow(getResizeUpStabilizationDelay());
+        
+        // this is the _sustained_ growth value; the smallest size that has been requested in the "stable-for-growing" period
+        long maxDesiredPoolSize = upsizeSummary.min;
+        boolean stableForGrowing = upsizeSummary.stableForGrowth;
+        
+        // this is the _sustained_ shrink value; largest size that has been requested in the "stable-for-shrinking" period:
+        long minDesiredPoolSize = downsizeSummary.max;
+        boolean stableForShrinking = downsizeSummary.stableForShrinking;
+        
+        // (it is a logical consequence of the above that minDesired >= maxDesired -- this is correct, if confusing:
+        // think of minDesired as the minimum size we are allowed to resize to, and similarly for maxDesired; 
+        // if min > max we can scale to max if current < max, or scale to min if current > min)
+
+        long desiredPoolSize;
+        
+        boolean stable;
+        
+        if (currentPoolSize < maxDesiredPoolSize) {
+            // we have valid request to grow 
+            // (we'll never have a valid request to grow and a valid to shrink simultaneously, btw)
+            desiredPoolSize = maxDesiredPoolSize;
+            stable = stableForGrowing;
+        } else if (currentPoolSize > minDesiredPoolSize) {
+            // we have valid request to shrink
+            desiredPoolSize = minDesiredPoolSize;
+            stable = stableForShrinking;
+        } else {
+            desiredPoolSize = currentPoolSize;
+            stable = stableForGrowing && stableForShrinking;
+        }
+
+        if (LOG.isTraceEnabled()) LOG.trace("{} calculated desired pool size: from {} to {}; minDesired {}, maxDesired {}; " +
+                "stable {}; now {}; downsizeHistory {}; upsizeHistory {}", 
+                new Object[] {this, currentPoolSize, desiredPoolSize, minDesiredPoolSize, maxDesiredPoolSize, stable, now, downsizeSummary, upsizeSummary});
+        
+        return new CalculatedDesiredPoolSize(desiredPoolSize, stable);
+    }
+    
+    private static class CalculatedDesiredPoolSize {
+        final long size;
+        final boolean stable;
+        
+        CalculatedDesiredPoolSize(long size, boolean stable) {
+            this.size = size;
+            this.stable = stable;
+        }
+    }
+    
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + (groovyTruth(name) ? "("+name+")" : "");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/MaxPoolSizeReachedEvent.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/MaxPoolSizeReachedEvent.java b/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/MaxPoolSizeReachedEvent.java
new file mode 100644
index 0000000..6e97771
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/MaxPoolSizeReachedEvent.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.autoscaling;
+
+import java.io.Serializable;
+
+import com.google.common.base.Objects;
+
+public class MaxPoolSizeReachedEvent implements Serializable {
+    private static final long serialVersionUID = 1602627701360505190L;
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+        protected long maxAllowed;
+        protected long currentPoolSize;
+        protected long currentUnbounded;
+        protected long maxUnbounded;
+        protected long timeWindow;
+        
+        public Builder maxAllowed(long val) {
+            this.maxAllowed = val; return this;
+        }
+
+        public Builder currentPoolSize(long val) {
+            this.currentPoolSize = val; return this;
+        }
+
+        public Builder currentUnbounded(long val) {
+            this.currentUnbounded = val; return this;
+        }
+
+        public Builder maxUnbounded(long val) {
+            this.maxUnbounded = val; return this;
+        }
+
+        public Builder timeWindow(long val) {
+            this.timeWindow = val; return this;
+        }
+        public MaxPoolSizeReachedEvent build() {
+            return new MaxPoolSizeReachedEvent(this);
+        }
+    }
+    
+    private final long maxAllowed;
+    private final long currentPoolSize;
+    private final long currentUnbounded;
+    private final long maxUnbounded;
+    private final long timeWindow;
+    
+    protected MaxPoolSizeReachedEvent(Builder builder) {
+        maxAllowed = builder.maxAllowed;
+        currentPoolSize = builder.currentPoolSize;
+        currentUnbounded = builder.currentUnbounded;
+        maxUnbounded = builder.maxUnbounded;
+        timeWindow = builder.timeWindow;
+    }
+    
+    public long getMaxAllowed() {
+        return maxAllowed;
+    }
+    
+    public long getCurrentPoolSize() {
+        return currentPoolSize;
+    }
+    
+    public long getCurrentUnbounded() {
+        return currentUnbounded;
+    }
+    
+    public long getMaxUnbounded() {
+        return maxUnbounded;
+    }
+    
+    public long getTimeWindow() {
+        return timeWindow;
+    }
+    
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this).add("maxAllowed", maxAllowed).add("currentPoolSize", currentPoolSize)
+                .add("currentUnbounded", currentUnbounded).add("maxUnbounded", maxUnbounded)
+                .add("timeWindow", timeWindow).toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/ResizeOperator.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/ResizeOperator.java b/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/ResizeOperator.java
new file mode 100644
index 0000000..4f4fbb0
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/ResizeOperator.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.autoscaling;
+
+import org.apache.brooklyn.api.entity.Entity;
+
+public interface ResizeOperator {
+
+    /**
+     * Resizes the given entity to the desired size, if possible.
+     * 
+     * @return the new size of the entity
+     */
+    public Integer resize(Entity entity, Integer desiredSize);
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/SizeHistory.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/SizeHistory.java b/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/SizeHistory.java
new file mode 100644
index 0000000..0aa8801
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/autoscaling/SizeHistory.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.autoscaling;
+
+import java.util.List;
+
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.collections.TimeWindowedList;
+import brooklyn.util.collections.TimestampedValue;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Objects;
+
+/**
+ * Using a {@link TimeWindowedList}, tracks the recent history of values to allow a summary of 
+ * those values to be obtained. 
+ *   
+ * @author aled
+ */
+public class SizeHistory {
+
+    public static class WindowSummary {
+        /** The most recent value (or -1 if there has been no value) */
+        public final long latest;
+        
+        /** The minimum vaule within the given time period */
+        public final long min;
+        
+        /** The maximum vaule within the given time period */
+        public final long max;
+        
+        /** true if, since that max value, there have not been any higher values */
+        public final boolean stableForGrowth;
+        
+        /** true if, since that low value, there have not been any lower values */
+        public final boolean stableForShrinking;
+        
+        public WindowSummary(long latest, long min, long max, boolean stableForGrowth, boolean stableForShrinking) {
+            this.latest = latest;
+            this.min = min;
+            this.max = max;
+            this.stableForGrowth = stableForGrowth;
+            this.stableForShrinking = stableForShrinking;
+        }
+        
+        @Override
+        public String toString() {
+            return Objects.toStringHelper(this).add("latest", latest).add("min", min).add("max", max)
+                    .add("stableForGrowth", stableForGrowth).add("stableForShrinking", stableForShrinking).toString();
+        }
+    }
+    
+    private final TimeWindowedList<Number> recentDesiredResizes;
+    
+    public SizeHistory(long windowSize) {
+        recentDesiredResizes = new TimeWindowedList<Number>(MutableMap.of("timePeriod", windowSize, "minExpiredVals", 1));
+    }
+
+    public void add(final int val) {
+        recentDesiredResizes.add(val);
+    }
+
+    public void setWindowSize(Duration newWindowSize) {
+        recentDesiredResizes.setTimePeriod(newWindowSize);
+    }
+    
+    /**
+     * Summarises the history of values in this time window, with a few special things:
+     * <ul>
+     *   <li>If entire time-window is not covered by the given values, then min is Integer.MIN_VALUE and max is Integer.MAX_VALUE 
+     *   <li>If no values, then latest is -1
+     *   <li>If no recent values, then keeps last-seen value (no matter how old), to use that
+     *   <li>"stable for growth" means that since that max value, there have not been any higher values
+     *   <li>"stable for shrinking" means that since that low value, there have not been any lower values
+     * </ul>
+     */
+    public WindowSummary summarizeWindow(Duration windowSize) {
+        long now = System.currentTimeMillis();
+        List<TimestampedValue<Number>> windowVals = recentDesiredResizes.getValuesInWindow(now, windowSize);
+        
+        Number latestObj = latestInWindow(windowVals);
+        long latest = (latestObj == null) ? -1: latestObj.longValue();
+        long max = maxInWindow(windowVals, windowSize).longValue();
+        long min = minInWindow(windowVals, windowSize).longValue();
+        
+        // TODO Could do more sophisticated "stable" check; this is the easiest code - correct but not most efficient
+        // in terms of the caller having to schedule additional stability checks.
+        boolean stable = (min == max);
+        
+        return new WindowSummary(latest, min, max, stable, stable);
+    }
+    
+    /**
+     * If the entire time-window is not covered by the given values, then returns Integer.MAX_VALUE.
+     */
+    private <T extends Number> T maxInWindow(List<TimestampedValue<T>> vals, Duration timeWindow) {
+        // TODO bad casting from Integer default result to T
+        long now = System.currentTimeMillis();
+        long epoch = now - timeWindow.toMilliseconds();
+        T result = null;
+        double resultAsDouble = Integer.MAX_VALUE;
+        for (TimestampedValue<T> val : vals) {
+            T valAsNum = val.getValue();
+            double valAsDouble = (valAsNum != null) ? valAsNum.doubleValue() : 0;
+            if (result == null && val.getTimestamp() > epoch) {
+                result = withDefault(null, Integer.MAX_VALUE);
+                resultAsDouble = result.doubleValue();
+            }
+            if (result == null || (valAsNum != null && valAsDouble > resultAsDouble)) {
+                result = valAsNum;
+                resultAsDouble = valAsDouble;
+            }
+        }
+        return withDefault(result, Integer.MAX_VALUE);
+    }
+    
+    /**
+     * If the entire time-window is not covered by the given values, then returns Integer.MIN_VALUE
+     */
+    private <T extends Number> T minInWindow(List<TimestampedValue<T>> vals, Duration timeWindow) {
+        long now = System.currentTimeMillis();
+        long epoch = now - timeWindow.toMilliseconds();
+        T result = null;
+        double resultAsDouble = Integer.MIN_VALUE;
+        for (TimestampedValue<T> val : vals) {
+            T valAsNum = val.getValue();
+            double valAsDouble = (valAsNum != null) ? valAsNum.doubleValue() : 0;
+            if (result == null && val.getTimestamp() > epoch) {
+                result = withDefault(null, Integer.MIN_VALUE);
+                resultAsDouble = result.doubleValue();
+            }
+            if (result == null || (val.getValue() != null && valAsDouble < resultAsDouble)) {
+                result = valAsNum;
+                resultAsDouble = valAsDouble;
+            }
+        }
+        return withDefault(result, Integer.MIN_VALUE);
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> T withDefault(T result, Integer defaultValue) {
+        return result!=null ? result : (T) defaultValue;
+    }
+    /**
+     * @return null if empty, or the most recent value
+     */
+    private <T extends Number> T latestInWindow(List<TimestampedValue<T>> vals) {
+        return vals.isEmpty() ? null : vals.get(vals.size()-1).getValue();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/followthesun/DefaultFollowTheSunModel.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/followthesun/DefaultFollowTheSunModel.java b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/DefaultFollowTheSunModel.java
new file mode 100644
index 0000000..fef3d7f
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/DefaultFollowTheSunModel.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.location.basic.AbstractLocation;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+
+public class DefaultFollowTheSunModel<ContainerType, ItemType> implements FollowTheSunModel<ContainerType, ItemType> {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultFollowTheSunModel.class);
+    
+    // Concurrent maps cannot have null value; use this to represent when no container is supplied for an item 
+    private static final String NULL = "null-val";
+    private static final Location NULL_LOCATION = new AbstractLocation(newHashMap("name","null-location")) {};
+    
+    private final String name;
+    private final Set<ContainerType> containers = Collections.newSetFromMap(new ConcurrentHashMap<ContainerType,Boolean>());
+    private final Map<ItemType, ContainerType> itemToContainer = new ConcurrentHashMap<ItemType, ContainerType>();
+    private final Map<ContainerType, Location> containerToLocation = new ConcurrentHashMap<ContainerType, Location>();
+    private final Map<ItemType, Location> itemToLocation = new ConcurrentHashMap<ItemType, Location>();
+    private final Map<ItemType, Map<? extends ItemType, Double>> itemUsage = new ConcurrentHashMap<ItemType, Map<? extends ItemType,Double>>();
+    private final Set<ItemType> immovableItems = Collections.newSetFromMap(new ConcurrentHashMap<ItemType, Boolean>());
+
+    public DefaultFollowTheSunModel(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public Set<ItemType> getItems() {
+        return itemToContainer.keySet();
+    }
+    
+    @Override
+    public ContainerType getItemContainer(ItemType item) {
+        ContainerType result = itemToContainer.get(item);
+        return (isNull(result) ? null : result);
+    }
+    
+    @Override
+    public Location getItemLocation(ItemType item) {
+        Location result = itemToLocation.get(item);
+        return (isNull(result) ? null : result);
+    }
+    
+    @Override
+    public Location getContainerLocation(ContainerType container) {
+        Location result = containerToLocation.get(container);
+        return (isNull(result) ? null : result);
+    }
+    
+    // Provider methods.
+    
+    @Override public String getName() {
+        return name;
+    }
+    
+    // TODO: delete?
+    @Override public String getName(ItemType item) {
+        return item.toString();
+    }
+    
+    @Override public boolean isItemMoveable(ItemType item) {
+        // If don't know about item, then assume not movable; otherwise has this item been explicitly flagged as immovable?
+        return hasItem(item) && !immovableItems.contains(item);
+    }
+    
+    @Override public boolean isItemAllowedIn(ItemType item, Location location) {
+        return true; // TODO?
+    }
+    
+    @Override public boolean hasActiveMigration(ItemType item) {
+        return false; // TODO?
+    }
+    
+    @Override
+    // FIXME Too expensive to compute; store in a different data structure?
+    public Map<ItemType, Map<Location, Double>> getDirectSendsToItemByLocation() {
+        Map<ItemType, Map<Location, Double>> result = new LinkedHashMap<ItemType, Map<Location,Double>>(getNumItems());
+        
+        for (Map.Entry<ItemType, Map<? extends ItemType, Double>> entry : itemUsage.entrySet()) {
+            ItemType targetItem = entry.getKey();
+            Map<? extends ItemType, Double> sources = entry.getValue();
+            if (sources.isEmpty()) continue; // no-one talking to us
+            
+            Map<Location, Double> targetUsageByLocation = new LinkedHashMap<Location, Double>();
+            result.put(targetItem, targetUsageByLocation);
+
+            for (Map.Entry<? extends ItemType, Double> entry2 : sources.entrySet()) {
+                ItemType sourceItem = entry2.getKey();
+                Location sourceLocation = getItemLocation(sourceItem);
+                double usageVal = (entry.getValue() != null) ? entry2.getValue() : 0d;
+                if (sourceLocation == null) continue; // don't know where to attribute this load; e.g. item may have just terminated
+                if (sourceItem.equals(targetItem)) continue; // ignore msgs to self
+                
+                Double usageValTotal = targetUsageByLocation.get(sourceLocation);
+                double newUsageValTotal = (usageValTotal != null ? usageValTotal : 0d) + usageVal;
+                targetUsageByLocation.put(sourceLocation, newUsageValTotal);
+            }
+        }
+        
+        return result;
+    }
+    
+    @Override
+    public Set<ContainerType> getAvailableContainersFor(ItemType item, Location location) {
+        checkNotNull(location);
+        return getContainersInLocation(location);
+    }
+
+
+    // Mutators.
+    
+    @Override
+    public void onItemMoved(ItemType item, ContainerType newContainer) {
+        // idempotent, as may be called multiple times
+        Location newLocation = (newContainer != null) ? containerToLocation.get(newContainer) : null;
+        ContainerType newContainerNonNull = toNonNullContainer(newContainer);
+        Location newLocationNonNull = toNonNullLocation(newLocation);
+        ContainerType oldContainer = itemToContainer.put(item, newContainerNonNull);
+        Location oldLocation = itemToLocation.put(item, newLocationNonNull);
+    }
+    
+    @Override
+    public void onContainerAdded(ContainerType container, Location location) {
+        Location locationNonNull = toNonNullLocation(location);
+        containers.add(container);
+        containerToLocation.put(container, locationNonNull);
+        for (ItemType item : getItemsOnContainer(container)) {
+            itemToLocation.put(item, locationNonNull);
+        }
+    }
+    
+    @Override
+    public void onContainerRemoved(ContainerType container) {
+        containers.remove(container);
+        containerToLocation.remove(container);
+    }
+    
+    public void onContainerLocationUpdated(ContainerType container, Location location) {
+        if (!containers.contains(container)) {
+            // unknown container; probably just stopped? 
+            // If this overtook onContainerAdded, then assume we'll lookup the location and get it right in onContainerAdded
+            if (LOG.isDebugEnabled()) LOG.debug("Ignoring setting of location for unknown container {}, to {}", container, location);
+            return;
+        }
+        Location locationNonNull = toNonNullLocation(location);
+        containerToLocation.put(container, locationNonNull);
+        for (ItemType item : getItemsOnContainer(container)) {
+            itemToLocation.put(item, locationNonNull);
+        }
+    }
+
+    @Override
+    public void onItemAdded(ItemType item, ContainerType container, boolean immovable) {
+        // idempotent, as may be called multiple times
+        
+        if (immovable) {
+            immovableItems.add(item);
+        }
+        Location location = (container != null) ? containerToLocation.get(container) : null;
+        ContainerType containerNonNull = toNonNullContainer(container);
+        Location locationNonNull = toNonNullLocation(location);
+        ContainerType oldContainer = itemToContainer.put(item, containerNonNull);
+        Location oldLocation = itemToLocation.put(item, locationNonNull);
+    }
+    
+    @Override
+    public void onItemRemoved(ItemType item) {
+        itemToContainer.remove(item);
+        itemToLocation.remove(item);
+        itemUsage.remove(item);
+        immovableItems.remove(item);
+    }
+    
+    @Override
+    public void onItemUsageUpdated(ItemType item, Map<? extends ItemType, Double> newValue) {
+        if (hasItem(item)) {
+            itemUsage.put(item, newValue);
+        } else {
+            // Can happen when item removed - get notification of removal and workrate from group and item
+            // respectively, so can overtake each other
+            if (LOG.isDebugEnabled()) LOG.debug("Ignoring setting of usage for unknown item {}, to {}", item, newValue);
+        }
+    }
+    
+    
+    // Additional methods for tests.
+
+    /**
+     * Warning: this can be an expensive (time and memory) operation if there are a lot of items/containers. 
+     */
+    @VisibleForTesting
+    public String itemDistributionToString() {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        dumpItemDistribution(new PrintStream(baos));
+        return new String(baos.toByteArray());
+    }
+
+    @VisibleForTesting
+    public void dumpItemDistribution() {
+        dumpItemDistribution(System.out);
+    }
+    
+    @VisibleForTesting
+    public void dumpItemDistribution(PrintStream out) {
+        Map<ItemType, Map<Location, Double>> directSendsToItemByLocation = getDirectSendsToItemByLocation();
+        
+        out.println("Follow-The-Sun dump: ");
+        for (Location location: getLocations()) {
+            out.println("\t"+"Location "+location);
+            for (ContainerType container : getContainersInLocation(location)) {
+                out.println("\t\t"+"Container "+container);
+                for (ItemType item : getItemsOnContainer(container)) {
+                    Map<Location, Double> inboundUsage = directSendsToItemByLocation.get(item);
+                    Map<? extends ItemType, Double> outboundUsage = itemUsage.get(item);
+                    double totalInboundByLocation = (inboundUsage != null) ? sum(inboundUsage.values()) : 0d;
+                    double totalInboundByActor = (outboundUsage != null) ? sum(outboundUsage.values()) : 0d;
+                    out.println("\t\t\t"+"Item "+item);
+                    out.println("\t\t\t\t"+"Inbound-by-location: "+totalInboundByLocation+": "+inboundUsage);
+                    out.println("\t\t\t\t"+"Inbound-by-actor: "+totalInboundByActor+": "+outboundUsage);
+                }
+            }
+        }
+        out.flush();
+    }
+    
+    private boolean hasItem(ItemType item) {
+        return itemToContainer.containsKey(item);
+    }
+    
+    private Set<Location> getLocations() {
+        return ImmutableSet.copyOf(containerToLocation.values());
+    }
+    
+    private Set<ContainerType> getContainersInLocation(Location location) {
+        Set<ContainerType> result = new LinkedHashSet<ContainerType>();
+        for (Map.Entry<ContainerType, Location> entry : containerToLocation.entrySet()) {
+            if (location.equals(entry.getValue())) {
+                result.add(entry.getKey());
+            }
+        }
+        return result;
+    }
+    
+    private Set<ItemType> getItemsOnContainer(ContainerType container) {
+        Set<ItemType> result = new LinkedHashSet<ItemType>();
+        for (Map.Entry<ItemType, ContainerType> entry : itemToContainer.entrySet()) {
+            if (container.equals(entry.getValue())) {
+                result.add(entry.getKey());
+            }
+        }
+        return result;
+    }
+    
+    private int getNumItems() {
+        return itemToContainer.size();
+    }
+    
+    @SuppressWarnings("unchecked")
+    private ContainerType nullContainer() {
+        return (ContainerType) NULL; // relies on erasure
+    }
+    
+    private Location nullLocation() {
+        return NULL_LOCATION;
+    }
+    
+    private ContainerType toNonNullContainer(ContainerType val) {
+        return (val != null) ? val : nullContainer();
+    }
+    
+    private Location toNonNullLocation(Location val) {
+        return (val != null) ? val : nullLocation();
+    }
+    
+    private boolean isNull(Object val) {
+        return val == NULL || val == NULL_LOCATION;
+    }
+    
+    // TODO Move to utils; or stop AbstractLocation from removing things from the map!
+    public static <K,V> Map<K,V> newHashMap(K k, V v) {
+        Map<K,V> result = Maps.newLinkedHashMap();
+        result.put(k, v);
+        return result;
+    }
+    
+    public static double sum(Collection<? extends Number> values) {
+        double total = 0;
+        for (Number d : values) {
+            total += d.doubleValue();
+        }
+        return total;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunModel.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunModel.java b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunModel.java
new file mode 100644
index 0000000..07c6ed0
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/followthesun/FollowTheSunModel.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.location.Location;
+
+/**
+ * Captures the state of items, containers and locations for the purpose of moving items around
+ * to minimise latency. For consumption by a {@link FollowTheSunStrategy}.
+ */
+public interface FollowTheSunModel<ContainerType, ItemType> {
+
+    // Attributes of the pool.
+    public String getName();
+    
+    // Attributes of containers and items.
+    public String getName(ItemType item);
+    public Set<ItemType> getItems();
+    public Map<ItemType, Map<Location, Double>> getDirectSendsToItemByLocation();
+    public Location getItemLocation(ItemType item);
+    public ContainerType getItemContainer(ItemType item);
+    public Location getContainerLocation(ContainerType container);
+    public boolean hasActiveMigration(ItemType item);
+    public Set<ContainerType> getAvailableContainersFor(ItemType item, Location location);
+    public boolean isItemMoveable(ItemType item);
+    public boolean isItemAllowedIn(ItemType item, Location location);
+    
+    // Mutators for keeping the model in-sync with the observed world
+    public void onContainerAdded(ContainerType container, Location location);
+    public void onContainerRemoved(ContainerType container);
+    public void onContainerLocationUpdated(ContainerType container, Location location);
+
+    public void onItemAdded(ItemType item, ContainerType parentContainer, boolean immovable);
+    public void onItemRemoved(ItemType item);
+    public void onItemUsageUpdated(ItemType item, Map<? extends ItemType, Double> newValues);
+    public void onItemMoved(ItemType item, ContainerType newContainer);
+}


[12/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyMetricTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyMetricTest.java b/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyMetricTest.java
deleted file mode 100644
index 174b982..0000000
--- a/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyMetricTest.java
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.autoscaling;
-
-import static brooklyn.policy.autoscaling.AutoScalerPolicyTest.currentSizeAsserter;
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertTrue;
-
-import java.util.List;
-
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.event.AttributeSensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.apache.brooklyn.test.entity.TestCluster;
-import org.apache.brooklyn.test.entity.TestEntity;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-import brooklyn.event.basic.BasicNotificationSensor;
-import brooklyn.event.basic.Sensors;
-import brooklyn.test.Asserts;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
-
-public class AutoScalerPolicyMetricTest {
-    
-    private static long TIMEOUT_MS = 10000;
-    private static long SHORT_WAIT_MS = 50;
-    
-    private static final AttributeSensor<Integer> MY_ATTRIBUTE = Sensors.newIntegerSensor("autoscaler.test.intAttrib");
-    TestApplication app;
-    TestCluster tc;
-    
-    @BeforeMethod(alwaysRun=true)
-    public void before() {
-        app = TestApplication.Factory.newManagedInstanceForTests();
-        tc = app.createAndManageChild(EntitySpec.create(TestCluster.class)
-                .configure("initialSize", 1));
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        if (app != null) Entities.destroyAll(app.getManagementContext());
-    }
-
-    @Test
-    public void testIncrementsSizeIffUpperBoundExceeded() {
-        tc.resize(1);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
-        tc.addPolicy(policy);
-
-        tc.setAttribute(MY_ATTRIBUTE, 100);
-        Asserts.succeedsContinually(ImmutableMap.of("timeout", SHORT_WAIT_MS), currentSizeAsserter(tc, 1));
-
-        tc.setAttribute(MY_ATTRIBUTE, 101);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
-    }
-    
-    @Test
-    public void testDecrementsSizeIffLowerBoundExceeded() {
-        tc.resize(2);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
-        tc.addPolicy(policy);
-
-        tc.setAttribute(MY_ATTRIBUTE, 50);
-        Asserts.succeedsContinually(ImmutableMap.of("timeout", SHORT_WAIT_MS), currentSizeAsserter(tc, 2));
-
-        tc.setAttribute(MY_ATTRIBUTE, 49);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 1));
-    }
-    
-    @Test(groups="Integration")
-    public void testIncrementsSizeInProportionToMetric() {
-        tc.resize(5);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
-        tc.addPolicy(policy);
-        
-        // workload 200 so requires doubling size to 10 to handle: (200*5)/100 = 10
-        tc.setAttribute(MY_ATTRIBUTE, 200);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 10));
-        
-        // workload 5, requires 1 entity: (10*110)/100 = 11
-        tc.setAttribute(MY_ATTRIBUTE, 110);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 11));
-    }
-    
-    @Test(groups="Integration")
-    public void testDecrementsSizeInProportionToMetric() {
-        tc.resize(5);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
-        tc.addPolicy(policy);
-        
-        // workload can be handled by 4 servers, within its valid range: (49*5)/50 = 4.9
-        tc.setAttribute(MY_ATTRIBUTE, 49);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 4));
-        
-        // workload can be handled by 4 servers, within its valid range: (25*4)/50 = 2
-        tc.setAttribute(MY_ATTRIBUTE, 25);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
-        
-        tc.setAttribute(MY_ATTRIBUTE, 0);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 1));
-    }
-    
-    @Test(groups="Integration")
-    public void testObeysMinAndMaxSize() {
-        tc.resize(4);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
-                .metricLowerBound(50).metricUpperBound(100)
-                .minPoolSize(2).maxPoolSize(6)
-                .build();
-        tc.addPolicy(policy);
-
-        // Decreases to min-size only
-        tc.setAttribute(MY_ATTRIBUTE, 0);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
-        
-        // Increases to max-size only
-        tc.setAttribute(MY_ATTRIBUTE, 100000);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 6));
-    }
-    
-    @Test(groups="Integration",invocationCount=20)
-    public void testWarnsWhenMaxCapReached() {
-        final List<MaxPoolSizeReachedEvent> maxReachedEvents = Lists.newCopyOnWriteArrayList();
-        tc.resize(1);
-        
-        BasicNotificationSensor<MaxPoolSizeReachedEvent> maxSizeReachedSensor = AutoScalerPolicy.DEFAULT_MAX_SIZE_REACHED_SENSOR;
-        
-        app.subscribe(tc, maxSizeReachedSensor, new SensorEventListener<MaxPoolSizeReachedEvent>() {
-                @Override public void onEvent(SensorEvent<MaxPoolSizeReachedEvent> event) {
-                    maxReachedEvents.add(event.getValue());
-                }});
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
-                .metricLowerBound(50).metricUpperBound(100)
-                .maxPoolSize(6)
-                .maxSizeReachedSensor(maxSizeReachedSensor)
-                .build();
-        tc.addPolicy(policy);
-
-        // workload can be handled by 6 servers, so no need to notify: 6 <= (100*6)/50
-        tc.setAttribute(MY_ATTRIBUTE, 600);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 6));
-        assertTrue(maxReachedEvents.isEmpty());
-        
-        // Increases to above max capacity: would require (100000*6)/100 = 6000
-        tc.setAttribute(MY_ATTRIBUTE, 100000);
-        
-        // Assert our listener gets notified (once)
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            public void run() {
-                assertEquals(maxReachedEvents.size(), 1);
-                assertEquals(maxReachedEvents.get(0).getMaxAllowed(), 6);
-                assertEquals(maxReachedEvents.get(0).getCurrentPoolSize(), 6);
-                assertEquals(maxReachedEvents.get(0).getCurrentUnbounded(), 6000);
-                assertEquals(maxReachedEvents.get(0).getMaxUnbounded(), 6000);
-                assertEquals(maxReachedEvents.get(0).getTimeWindow(), 0);
-            }});
-        Asserts.succeedsContinually(new Runnable() {
-                @Override public void run() {
-                    assertEquals(maxReachedEvents.size(), 1);
-                }});
-        currentSizeAsserter(tc, 6).run();
-    }
-    
-    @Test
-    public void testDestructionState() {
-        tc.resize(1);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
-        tc.addPolicy(policy);
-
-        policy.destroy();
-        assertTrue(policy.isDestroyed());
-        assertFalse(policy.isRunning());
-        
-        tc.setAttribute(MY_ATTRIBUTE, 100000);
-        Asserts.succeedsContinually(ImmutableMap.of("timeout", SHORT_WAIT_MS), currentSizeAsserter(tc, 1));
-        
-        // TODO Could assert all subscriptions have been de-registered as well, 
-        // but that requires exposing more things just for testing...
-    }
-    
-    @Test
-    public void testSuspendState() {
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
-        tc.addPolicy(policy);
-        
-        policy.suspend();
-        assertFalse(policy.isRunning());
-        assertFalse(policy.isDestroyed());
-        
-        policy.resume();
-        assertTrue(policy.isRunning());
-        assertFalse(policy.isDestroyed());
-    }
-
-    @Test
-    public void testPostSuspendActions() {
-        tc.resize(1);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
-        tc.addPolicy(policy);
-
-        policy.suspend();
-        
-        tc.setAttribute(MY_ATTRIBUTE, 100000);
-        Asserts.succeedsContinually(ImmutableMap.of("timeout", SHORT_WAIT_MS), currentSizeAsserter(tc, 1));
-    }
-    
-    @Test
-    public void testPostResumeActions() {
-        tc.resize(1);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE).metricLowerBound(50).metricUpperBound(100).build();
-        tc.addPolicy(policy);
-        
-        policy.suspend();
-        policy.resume();
-        tc.setAttribute(MY_ATTRIBUTE, 101);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
-    }
-    
-    @Test
-    public void testSubscribesToMetricOnSpecifiedEntity() {
-        TestEntity entityWithMetric = app.createAndManageChild(EntitySpec.create(TestEntity.class));
-        
-        tc.resize(1);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder()
-                .metric(TestEntity.SEQUENCE)
-                .entityWithMetric(entityWithMetric)
-                .metricLowerBound(50)
-                .metricUpperBound(100)
-                .build();
-        tc.addPolicy(policy);
-
-        // First confirm that tc is not being listened to for this entity
-        tc.setAttribute(TestEntity.SEQUENCE, 101);
-        Asserts.succeedsContinually(ImmutableMap.of("timeout", SHORT_WAIT_MS), currentSizeAsserter(tc, 1));
-
-        // Then confirm we listen to the correct "entityWithMetric"
-        entityWithMetric.setAttribute(TestEntity.SEQUENCE, 101);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyRebindTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyRebindTest.java b/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyRebindTest.java
deleted file mode 100644
index 03f3f1d..0000000
--- a/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyRebindTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.autoscaling;
-
-import static org.testng.Assert.assertEquals;
-
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.event.AttributeSensor;
-import org.apache.brooklyn.api.location.LocationSpec;
-import org.apache.brooklyn.test.EntityTestUtils;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.apache.brooklyn.test.entity.TestEntity;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.EntityInternal;
-import brooklyn.entity.group.DynamicCluster;
-import brooklyn.entity.rebind.RebindTestFixtureWithApp;
-import brooklyn.event.basic.BasicNotificationSensor;
-import brooklyn.event.basic.Sensors;
-
-import org.apache.brooklyn.location.basic.SimulatedLocation;
-
-import brooklyn.util.time.Duration;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-
-public class AutoScalerPolicyRebindTest extends RebindTestFixtureWithApp {
-
-    public static BasicNotificationSensor<Map> POOL_HOT_SENSOR = new BasicNotificationSensor<Map>(
-            Map.class, "AutoScalerPolicyRebindTest.resizablepool.hot", "Pool is over-utilized; it has insufficient resource for current workload");
-    public static BasicNotificationSensor<Map> POOL_COLD_SENSOR = new BasicNotificationSensor<Map>(
-            Map.class, "AutoScalerPolicyRebindTest.resizablepool.cold", "Pool is under-utilized; it has too much resource for current workload");
-    public static BasicNotificationSensor<Map> POOL_OK_SENSOR = new BasicNotificationSensor<Map>(
-            Map.class, "AutoScalerPolicyRebindTest.resizablepool.cold", "Pool utilization is ok; the available resources are fine for the current workload");
-    public static BasicNotificationSensor<MaxPoolSizeReachedEvent> MAX_SIZE_REACHED_SENSOR = new BasicNotificationSensor<MaxPoolSizeReachedEvent>(
-            MaxPoolSizeReachedEvent.class, "AutoScalerPolicyRebindTest.maxSizeReached");
-    public static AttributeSensor<Integer> METRIC_SENSOR = Sensors.newIntegerSensor("AutoScalerPolicyRebindTest.metric");
-            
-    private DynamicCluster origCluster;
-    private SimulatedLocation origLoc;
-
-    @BeforeMethod(alwaysRun=true)
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        origLoc = origManagementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
-        origCluster = origApp.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("memberSpec", EntitySpec.create(TestEntity.class)));
-    }
-    
-    @Test
-    public void testRestoresAutoScalerConfig() throws Exception {
-        origCluster.addPolicy(AutoScalerPolicy.builder()
-                .name("myname")
-                .metric(METRIC_SENSOR)
-                .entityWithMetric(origCluster)
-                .metricUpperBound(1)
-                .metricLowerBound(2)
-                .minPoolSize(0)
-                .maxPoolSize(3)
-                .minPeriodBetweenExecs(Duration.of(4, TimeUnit.MILLISECONDS))
-                .resizeUpStabilizationDelay(Duration.of(5, TimeUnit.MILLISECONDS))
-                .resizeDownStabilizationDelay(Duration.of(6, TimeUnit.MILLISECONDS))
-                .poolHotSensor(POOL_HOT_SENSOR)
-                .poolColdSensor(POOL_COLD_SENSOR)
-                .poolOkSensor(POOL_OK_SENSOR)
-                .maxSizeReachedSensor(MAX_SIZE_REACHED_SENSOR)
-                .maxReachedNotificationDelay(Duration.of(7, TimeUnit.MILLISECONDS))
-                .buildSpec());
-        
-        TestApplication newApp = rebind();
-        DynamicCluster newCluster = (DynamicCluster) Iterables.getOnlyElement(newApp.getChildren());
-        AutoScalerPolicy newPolicy = (AutoScalerPolicy) Iterables.getOnlyElement(newCluster.getPolicies());
-
-        assertEquals(newPolicy.getDisplayName(), "myname");
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.METRIC), METRIC_SENSOR);
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.ENTITY_WITH_METRIC), newCluster);
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.METRIC_UPPER_BOUND), 1);
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.METRIC_LOWER_BOUND), 2);
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.MIN_POOL_SIZE), (Integer)0);
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.MAX_POOL_SIZE), (Integer)3);
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.MIN_PERIOD_BETWEEN_EXECS), Duration.of(4, TimeUnit.MILLISECONDS));
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.RESIZE_UP_STABILIZATION_DELAY), Duration.of(5, TimeUnit.MILLISECONDS));
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.RESIZE_DOWN_STABILIZATION_DELAY), Duration.of(6, TimeUnit.MILLISECONDS));
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.POOL_HOT_SENSOR), POOL_HOT_SENSOR);
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.POOL_COLD_SENSOR), POOL_COLD_SENSOR);
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.POOL_OK_SENSOR), POOL_OK_SENSOR);
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.MAX_SIZE_REACHED_SENSOR), MAX_SIZE_REACHED_SENSOR);
-        assertEquals(newPolicy.getConfig(AutoScalerPolicy.MAX_REACHED_NOTIFICATION_DELAY), Duration.of(7, TimeUnit.MILLISECONDS));
-    }
-    
-    @Test
-    public void testAutoScalerResizesAfterRebind() throws Exception {
-        origCluster.start(ImmutableList.of(origLoc));
-        origCluster.addPolicy(AutoScalerPolicy.builder()
-                .name("myname")
-                .metric(METRIC_SENSOR)
-                .entityWithMetric(origCluster)
-                .metricUpperBound(10)
-                .metricLowerBound(100)
-                .minPoolSize(1)
-                .maxPoolSize(3)
-                .buildSpec());
-        
-        TestApplication newApp = rebind();
-        DynamicCluster newCluster = (DynamicCluster) Iterables.getOnlyElement(newApp.getChildren());
-
-        assertEquals(newCluster.getCurrentSize(), (Integer)1);
-        
-        ((EntityInternal)newCluster).setAttribute(METRIC_SENSOR, 1000);
-        EntityTestUtils.assertGroupSizeEqualsEventually(newCluster, 3);
-        
-        ((EntityInternal)newCluster).setAttribute(METRIC_SENSOR, 1);
-        EntityTestUtils.assertGroupSizeEqualsEventually(newCluster, 1);
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyReconfigurationTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyReconfigurationTest.java b/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyReconfigurationTest.java
deleted file mode 100644
index e685735..0000000
--- a/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyReconfigurationTest.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.autoscaling;
-
-import static brooklyn.policy.autoscaling.AutoScalerPolicyTest.currentSizeAsserter;
-
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.event.AttributeSensor;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.apache.brooklyn.test.entity.TestCluster;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-import brooklyn.event.basic.Sensors;
-import brooklyn.test.Asserts;
-import brooklyn.util.time.Duration;
-
-import com.google.common.collect.ImmutableMap;
-
-public class AutoScalerPolicyReconfigurationTest {
-    
-    private static long TIMEOUT_MS = 10000;
-    
-    private static final AttributeSensor<Integer> MY_ATTRIBUTE = Sensors.newIntegerSensor("autoscaler.test.intAttrib");
-    TestApplication app;
-    TestCluster tc;
-    
-    @BeforeMethod(alwaysRun=true)
-    public void before() throws Exception {
-        app = TestApplication.Factory.newManagedInstanceForTests();
-        tc = app.createAndManageChild(EntitySpec.create(TestCluster.class)
-                .configure("initialSize", 1));
-    }
-
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        if (app != null) Entities.destroyAll(app.getManagementContext());
-    }
-
-    @Test
-    public void testIncreaseMinPoolSizeCausesImmediateGrowth() {
-        tc.resize(2);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
-                .metricLowerBound(50).metricUpperBound(100)
-                .minPoolSize(2)
-                .build();
-        tc.addPolicy(policy);
-
-        policy.config().set(AutoScalerPolicy.MIN_POOL_SIZE, 3);
-        
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 3));
-    }
-    
-    @Test
-    public void testDecreaseMinPoolSizeAllowsSubsequentShrink() {
-        tc.resize(4);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
-                .metricLowerBound(50).metricUpperBound(100)
-                .minPoolSize(2)
-                .build();
-        tc.addPolicy(policy);
-        
-        // 25*4 = 100 -> 2 nodes at 50 each
-        tc.setAttribute(MY_ATTRIBUTE, 25);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
-
-        // Decreases to new min-size
-        policy.config().set(AutoScalerPolicy.MIN_POOL_SIZE, 1);
-        tc.setAttribute(MY_ATTRIBUTE, 0);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 1));
-    }
-    
-    @Test
-    public void testDecreaseMaxPoolSizeCausesImmediateShrink() {
-        tc.resize(6);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
-                .metricLowerBound(50).metricUpperBound(100)
-                .maxPoolSize(6)
-                .build();
-        tc.addPolicy(policy);
-
-        policy.config().set(AutoScalerPolicy.MAX_POOL_SIZE, 4);
-        
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 4));
-    }
-    
-    @Test
-    public void testIncreaseMaxPoolSizeAllowsSubsequentGrowth() {
-        tc.resize(3);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
-                .metricLowerBound(50).metricUpperBound(100)
-                .maxPoolSize(6)
-                .build();
-        tc.addPolicy(policy);
-
-        // 200*3 = 600 -> 6 nodes at 100 each
-        tc.setAttribute(MY_ATTRIBUTE, 200);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 6));
-        
-        policy.config().set(AutoScalerPolicy.MAX_POOL_SIZE, 8);
-        
-        // Increases to max-size only
-        tc.setAttribute(MY_ATTRIBUTE, 100000);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 8));
-    }
-    
-    @Test
-    public void testReconfigureMetricLowerBound() {
-        tc.resize(2);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
-                .metricLowerBound(50).metricUpperBound(100)
-                .build();
-        tc.addPolicy(policy);
-
-        policy.config().set(AutoScalerPolicy.METRIC_LOWER_BOUND, 51);
-
-        tc.setAttribute(MY_ATTRIBUTE, 50);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 1));
-    }
-
-    @Test
-    public void testReconfigureMetricUpperBound() {
-        tc.resize(1);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
-                .metricLowerBound(50).metricUpperBound(100)
-                .build();
-        tc.addPolicy(policy);
-
-        policy.config().set(AutoScalerPolicy.METRIC_UPPER_BOUND, 99);
-
-        tc.setAttribute(MY_ATTRIBUTE, 100);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
-    }
-
-    @Test
-    public void testReconfigureResizeUpStabilizationDelay() {
-        tc.resize(1);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
-                .metricLowerBound(50).metricUpperBound(100)
-                .resizeUpStabilizationDelay(Duration.TWO_MINUTES)
-                .build();
-        tc.addPolicy(policy);
-
-        policy.config().set(AutoScalerPolicy.RESIZE_UP_STABILIZATION_DELAY, Duration.ZERO);
-
-        tc.setAttribute(MY_ATTRIBUTE, 101);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 2));
-    }
-    
-    @Test
-    public void testReconfigureResizeDownStabilizationDelay() {
-        tc.resize(2);
-        
-        AutoScalerPolicy policy = new AutoScalerPolicy.Builder().metric(MY_ATTRIBUTE)
-                .metricLowerBound(50).metricUpperBound(100)
-                .resizeDownStabilizationDelay(Duration.TWO_MINUTES)
-                .build();
-        tc.addPolicy(policy);
-
-        policy.config().set(AutoScalerPolicy.RESIZE_DOWN_STABILIZATION_DELAY, Duration.ZERO);
-
-        tc.setAttribute(MY_ATTRIBUTE, 1);
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(tc, 1));
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyTest.java b/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyTest.java
deleted file mode 100644
index a4724f1..0000000
--- a/policy/src/test/java/brooklyn/policy/autoscaling/AutoScalerPolicyTest.java
+++ /dev/null
@@ -1,649 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.autoscaling;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
-
-import java.lang.management.ManagementFactory;
-import java.lang.management.MemoryMXBean;
-import java.lang.management.MemoryUsage;
-import java.lang.management.OperatingSystemMXBean;
-import java.lang.management.ThreadInfo;
-import java.lang.management.ThreadMXBean;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.policy.PolicySpec;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.apache.brooklyn.test.entity.TestCluster;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.Assert;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.trait.Resizable;
-import brooklyn.event.basic.BasicNotificationSensor;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableList;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.time.Duration;
-
-import com.google.common.base.Stopwatch;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-
-public class AutoScalerPolicyTest {
-
-    private static final Logger log = LoggerFactory.getLogger(AutoScalerPolicyTest.class);
-    
-    private static long TIMEOUT_MS = 10*1000;
-    private static long SHORT_WAIT_MS = 250;
-    private static long OVERHEAD_DURATION_MS = 500;
-    private static long EARLY_RETURN_MS = 10;
-
-    private static final int MANY_TIMES_INVOCATION_COUNT = 10;
-    
-    AutoScalerPolicy policy;
-    TestCluster cluster;
-    LocallyResizableEntity resizable;
-    TestApplication app;
-    List<Integer> policyResizes = MutableList.of();
-    
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        log.info("resetting "+getClass().getSimpleName());
-        app = TestApplication.Factory.newManagedInstanceForTests();
-        cluster = app.createAndManageChild(EntitySpec.create(TestCluster.class).configure(TestCluster.INITIAL_SIZE, 1));
-        resizable = new LocallyResizableEntity(cluster, cluster);
-        Entities.manage(resizable);
-        PolicySpec<AutoScalerPolicy> policySpec = PolicySpec.create(AutoScalerPolicy.class).configure(AutoScalerPolicy.RESIZE_OPERATOR, new ResizeOperator() {
-            @Override
-            public Integer resize(Entity entity, Integer desiredSize) {
-                log.info("resizing to "+desiredSize);
-                policyResizes.add(desiredSize);
-                return ((Resizable)entity).resize(desiredSize);
-            }
-        });
-        policy = resizable.addPolicy(policySpec);
-        policyResizes.clear();
-    }
-
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        if (policy != null) policy.destroy();
-        if (app != null) Entities.destroyAll(app.getManagementContext());
-        cluster = null;
-        resizable = null;
-        policy = null;
-    }
-
-    public void assertSizeEventually(Integer targetSize) {
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, targetSize));
-        assertEquals(policyResizes.get(policyResizes.size()-1), targetSize);
-    }
-    
-    @Test
-    public void testShrinkColdPool() throws Exception {
-        resizable.resize(4);
-        // all metrics as per-node here
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(30d/4, 10, 20));
-        
-        // expect pool to shrink to 3 (i.e. maximum to have >= 10 per container)
-        assertSizeEventually(3);
-    }
-    
-    @Test
-    public void testShrinkColdPoolTotals() throws Exception {
-        resizable.resize(4);
-        // all metrics as totals here
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(30L, 4*10L, 4*20L));
-        
-        // expect pool to shrink to 3 (i.e. maximum to have >= 10 per container)
-        assertSizeEventually(3);
-    }
-    
-    @Test
-    public void testShrinkColdPoolRoundsUpDesiredNumberOfContainers() throws Exception {
-        resizable.resize(4);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(1L, 4*10L, 4*20L));
-        
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 1));
-    }
-
-    @Test
-    public void testGrowHotPool() throws Exception {
-        resizable.resize(2);
-        // all metrics as per-node here
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(21L, 10L, 20L));
-        
-        // expect pool to grow to 3 (i.e. minimum to have <= 20 per container)
-        assertSizeEventually(3);
-    }
-
-    @Test
-    public void testGrowHotPoolTotals() throws Exception {
-        resizable.resize(2);
-        // all metrics as totals here
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(41L, 2*10L, 2*20L));
-        
-        // expect pool to grow to 3 (i.e. minimum to have <= 20 per container)
-        assertSizeEventually(3);
-    }
-
-    @Test
-    public void testGrowShrinkRespectsResizeIterationIncrementAndResizeIterationMax() throws Exception {
-        resizable.resize(2);
-        policy.config().set(AutoScalerPolicy.RESIZE_UP_ITERATION_INCREMENT, 2);
-        policy.config().set(AutoScalerPolicy.RESIZE_UP_ITERATION_MAX, 4);
-        policy.config().set(AutoScalerPolicy.RESIZE_DOWN_ITERATION_INCREMENT, 3);
-        policy.config().set(AutoScalerPolicy.RESIZE_DOWN_ITERATION_MAX, 3);
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(42/2, 10, 20));
-        // expect pool to grow to 4 (i.e. to have <= 20 per container we need 3, but increment is 2)
-        assertSizeEventually(4);
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(200/4, 10, 20));
-        // a single hot message can only make it go to 8
-        assertSizeEventually(8);
-        assertEquals(policyResizes, MutableList.of(4, 8));
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(200/8, 10, 20));
-        assertSizeEventually(10);
-        assertEquals(policyResizes, MutableList.of(4, 8, 10));
-        
-        // now shrink
-        policyResizes.clear();
-        policy.config().set(AutoScalerPolicy.MIN_POOL_SIZE, 2);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(1, 10, 20));
-        assertSizeEventually(7);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(1, 10, 20));
-        assertSizeEventually(4);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(1, 10, 20));
-        assertSizeEventually(2);
-        assertEquals(policyResizes, MutableList.of(7, 4, 2));
-    }
-
-    @Test
-    public void testHasId() throws Exception {
-        resizable.removePolicy(policy);
-        policy = AutoScalerPolicy.builder()
-                .minPoolSize(2)
-                .build();
-        resizable.addPolicy(policy);
-        Assert.assertTrue(policy.getId()!=null);
-    }
-    
-    @Test
-    public void testNeverShrinkBelowMinimum() throws Exception {
-        resizable.removePolicy(policy);
-        policy = AutoScalerPolicy.builder()
-                .minPoolSize(2)
-                .build();
-        resizable.addPolicy(policy);
-        
-        resizable.resize(4);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 0L, 4*10L, 4*20L));
-        
-        // expect pool to shrink only to the minimum
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 2));
-    }
-    
-    @Test
-    public void testNeverGrowAboveMaximmum() throws Exception {
-        resizable.removePolicy(policy);
-        policy = AutoScalerPolicy.builder()
-                .maxPoolSize(5)
-                .build();
-        resizable.addPolicy(policy);
-        
-        resizable.resize(4);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(4, 1000000L, 4*10L, 4*20L));
-        
-        // expect pool to grow only to the maximum
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 5));
-    }
-    
-    @Test
-    public void testNeverGrowColdPool() throws Exception {
-        resizable.resize(2);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(2, 1000L, 2*10L, 2*20L));
-        
-        Thread.sleep(SHORT_WAIT_MS);
-        assertEquals(resizable.getCurrentSize(), (Integer)2);
-    }
-    
-    @Test
-    public void testNeverShrinkHotPool() throws Exception {
-        resizable.resizeSleepTime = 0;
-        resizable.resize(2);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(2, 0L, 2*10L, 2*20L));
-        
-        // if had been a POOL_COLD, would have shrunk to 3
-        Thread.sleep(SHORT_WAIT_MS);
-        assertEquals(resizable.getCurrentSize(), (Integer)2);
-    }
-    
-    @Test(groups="Integration")
-    public void testConcurrentShrinkShrink() throws Exception {
-        resizable.resizeSleepTime = 250;
-        resizable.resize(4);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 30L, 4*10L, 4*20L));
-        // would cause pool to shrink to 3
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 1L, 4*10L, 4*20L));
-        // now expect pool to shrink to 1
-        
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 1));
-    }
-    
-    @Test(groups="Integration")
-    public void testConcurrentGrowGrow() throws Exception {
-        resizable.resizeSleepTime = 250;
-        resizable.resize(2);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(2, 41L, 2*10L, 2*20L));
-        // would cause pool to grow to 3
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(2, 81L, 2*10L, 2*20L));
-        // now expect pool to grow to 5
-        
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 5));
-    }
-    
-    @Test(groups="Integration")
-    public void testConcurrentGrowShrink() throws Exception {
-        resizable.resizeSleepTime = 250;
-        resizable.resize(2);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(2, 81L, 2*10L, 2*20L));
-        // would cause pool to grow to 5
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(2, 1L, 2*10L, 2*20L));
-        // now expect pool to shrink to 1
-        
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 1));
-    }
-    
-    @Test(groups="Integration")
-    public void testConcurrentShrinkGrow() throws Exception {
-        resizable.resizeSleepTime = 250;
-        resizable.resize(4);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 1L, 4*10L, 4*20L));
-        // would cause pool to shrink to 1
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(4, 81L, 4*10L, 4*20L));
-        // now expect pool to grow to 5
-        
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 5));
-    }
-    
-    // FIXME failed in jenkins (e.g. #1035); with "lists don't have the same size expected:<3> but was:<2>"
-    // Is it just too time sensitive? But I'd have expected > 3 rather than less
-    @Test(groups="WIP")
-    public void testRepeatedQueuedResizeTakesLatestValueRatherThanIntermediateValues() throws Exception {
-        // TODO is this too time sensitive? the resize takes only 250ms so if it finishes before the next emit we'd also see size=2
-        resizable.resizeSleepTime = 500;
-        resizable.resize(4);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 30L, 4*10L, 4*20L)); // shrink to 3
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 20L, 4*10L, 4*20L)); // shrink to 2
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 10L, 4*10L, 4*20L)); // shrink to 1
-        
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 1));
-        assertEquals(resizable.sizes, ImmutableList.of(4, 3, 1));
-    }
-    
-
-    @Test
-    public void testUsesResizeOperatorOverride() throws Exception {
-        resizable.removePolicy(policy);
-        
-        final AtomicInteger counter = new AtomicInteger();
-        policy = AutoScalerPolicy.builder()
-                .resizeOperator(new ResizeOperator() {
-                        @Override public Integer resize(Entity entity, Integer desiredSize) {
-                            counter.incrementAndGet();
-                            return desiredSize;
-                        }})
-                .build();
-        resizable.addPolicy(policy);
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(1, 21L, 1*10L, 1*20L)); // grow to 2
-        
-        Asserts.succeedsEventually(MutableMap.of("timeout",TIMEOUT_MS), new Runnable() {
-                public void run() {
-                    assertTrue(counter.get() >= 1, "cccounter="+counter);
-                }});
-    }
-    
-    @Test
-    public void testUsesCustomSensorOverride() throws Exception {
-        resizable.removePolicy(policy);
-        
-        @SuppressWarnings("rawtypes")
-        BasicNotificationSensor<Map> customPoolHotSensor = new BasicNotificationSensor<Map>(Map.class, "custom.hot", "");
-        @SuppressWarnings("rawtypes")
-        BasicNotificationSensor<Map> customPoolColdSensor = new BasicNotificationSensor<Map>(Map.class, "custom.cold", "");
-        @SuppressWarnings("rawtypes")
-        BasicNotificationSensor<Map> customPoolOkSensor = new BasicNotificationSensor<Map>(Map.class, "custom.ok", "");
-        policy = AutoScalerPolicy.builder()
-                .poolHotSensor(customPoolHotSensor) 
-                .poolColdSensor(customPoolColdSensor)
-                .poolOkSensor(customPoolOkSensor)
-                .build();
-        resizable.addPolicy(policy);
-        
-        resizable.emit(customPoolHotSensor, message(1, 21L, 1*10L, 1*20L)); // grow to 2
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 2));
-        
-        resizable.emit(customPoolColdSensor, message(2, 1L, 1*10L, 1*20L)); // shrink to 1
-        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 1));
-    }
-    
-    @Test(groups="Integration")
-    public void testResizeUpStabilizationDelayIgnoresBlip() throws Exception {
-        long resizeUpStabilizationDelay = 1000L;
-        Duration minPeriodBetweenExecs = Duration.ZERO;
-        resizable.removePolicy(policy);
-        
-        policy = AutoScalerPolicy.builder()
-                .resizeUpStabilizationDelay(Duration.of(resizeUpStabilizationDelay, TimeUnit.MILLISECONDS)) 
-                .minPeriodBetweenExecs(minPeriodBetweenExecs)
-                .build();
-        resizable.addPolicy(policy);
-        resizable.resize(1);
-        
-        // Ignores temporary blip
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(1, 61L, 1*10L, 1*20L)); // would grow to 4
-        Thread.sleep(resizeUpStabilizationDelay-OVERHEAD_DURATION_MS);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_OK_SENSOR, message(1, 11L, 4*10L, 4*20L)); // but 1 is still adequate
-        
-        assertEquals(resizable.getCurrentSize(), (Integer)1);
-        Asserts.succeedsContinually(MutableMap.of("duration", 2000L), new Runnable() {
-                @Override public void run() {
-                    assertEquals(resizable.sizes, ImmutableList.of(1));
-                }});
-    }
-
-    // FIXME failing in jenkins occassionally - have put it in the "Acceptance" group for now
-    //
-    // Error was things like it taking a couple of seconds too long to scale-up. This is *not*
-    // just caused by a slow GC (running with -verbose:gc shows during a failure several 
-    // incremental GCs that usually don't amount to more than 0.2 of a second at most, often less).
-    // Doing a thread-dump etc immediately after the too-long delay shows no strange thread usage,
-    // and shows releng3 system load averages of numbers like 1.73, 2.87 and 1.22.
-    // 
-    // Is healthy on normal machines.
-    @Test(groups={"Integration", "Acceptance"}, invocationCount=MANY_TIMES_INVOCATION_COUNT)
-    public void testRepeatedResizeUpStabilizationDelayTakesMaxSustainedDesired() throws Throwable {
-        try {
-            testResizeUpStabilizationDelayTakesMaxSustainedDesired();
-        } catch (Throwable t) {
-            dumpThreadsEtc();
-            throw t;
-        }
-    }
-
-    @Test(groups="Integration")
-    public void testResizeUpStabilizationDelayTakesMaxSustainedDesired() throws Exception {
-        long resizeUpStabilizationDelay = 1100L;
-        Duration minPeriodBetweenExecs = Duration.ZERO;
-        resizable.removePolicy(policy);
-        
-        policy = AutoScalerPolicy.builder()
-                .resizeUpStabilizationDelay(Duration.of(resizeUpStabilizationDelay, TimeUnit.MILLISECONDS)) 
-                .minPeriodBetweenExecs(minPeriodBetweenExecs)
-                .build();
-        resizable.addPolicy(policy);
-        resizable.resize(1);
-        
-        // Will grow to only the max sustained in this time window 
-        // (i.e. to 2 within the first $resizeUpStabilizationDelay milliseconds)
-        Stopwatch stopwatch = Stopwatch.createStarted();
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(1, 61L, 1*10L, 1*20L)); // would grow to 4
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(1, 21L, 1*10L, 1*20L)); // would grow to 2
-        Thread.sleep(resizeUpStabilizationDelay-OVERHEAD_DURATION_MS);
-        
-        long postSleepTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(1, 61L, 1*10L, 1*20L)); // would grow to 4
-
-        // Wait for it to reach size 2, and confirm take expected time
-        // TODO This is time sensitive, and sometimes fails in CI with size=4 if we wait for currentSize==2 (presumably GC kicking in?)
-        //      Therefore do strong assertion of currentSize==2 later, so can write out times if it goes wrong.
-        Asserts.succeedsEventually(MutableMap.of("period", 1, "timeout", TIMEOUT_MS), new Runnable() {
-            public void run() {
-                assertTrue(resizable.getCurrentSize() >= 2, "currentSize="+resizable.getCurrentSize());
-            }});
-        assertEquals(resizable.getCurrentSize(), (Integer)2, 
-                stopwatch.elapsed(TimeUnit.MILLISECONDS)+"ms after first emission; "+(stopwatch.elapsed(TimeUnit.MILLISECONDS)-postSleepTime)+"ms after last");
-        
-        long timeToResizeTo2 = stopwatch.elapsed(TimeUnit.MILLISECONDS);
-        assertTrue(timeToResizeTo2 >= resizeUpStabilizationDelay-EARLY_RETURN_MS &&
-                timeToResizeTo2 <= resizeUpStabilizationDelay+OVERHEAD_DURATION_MS,
-                "Resizing to 2: time="+timeToResizeTo2+"; resizeUpStabilizationDelay="+resizeUpStabilizationDelay);
-
-        // Will then grow to 4 $resizeUpStabilizationDelay milliseconds after that emission
-        Asserts.succeedsEventually(MutableMap.of("period", 1, "timeout", TIMEOUT_MS), 
-                currentSizeAsserter(resizable, 4));
-        long timeToResizeTo4 = stopwatch.elapsed(TimeUnit.MILLISECONDS) - postSleepTime;
-        
-        assertTrue(timeToResizeTo4 >= resizeUpStabilizationDelay-EARLY_RETURN_MS &&
-                timeToResizeTo4 <= resizeUpStabilizationDelay+OVERHEAD_DURATION_MS,
-                "Resizing to 4: timeToResizeTo4="+timeToResizeTo4+"; timeToResizeTo2="+timeToResizeTo2+"; resizeUpStabilizationDelay="+resizeUpStabilizationDelay);
-    }
-
-    @Test(groups="Integration")
-    public void testResizeUpStabilizationDelayResizesAfterDelay() {
-        final long resizeUpStabilizationDelay = 1000L;
-        Duration minPeriodBetweenExecs = Duration.ZERO;
-        resizable.removePolicy(policy);
-        
-        policy = resizable.addPolicy(AutoScalerPolicy.builder()
-                .resizeUpStabilizationDelay(Duration.of(resizeUpStabilizationDelay, TimeUnit.MILLISECONDS)) 
-                .minPeriodBetweenExecs(minPeriodBetweenExecs)
-                .buildSpec());
-        resizable.resize(1);
-        
-        // After suitable delay, grows to desired
-        final long emitTime = System.currentTimeMillis();
-        final Map<String, Object> need4 = message(1, 61L, 1*10L, 1*20L);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, need4); // would grow to 4
-        final AtomicInteger emitCount = new AtomicInteger(0);
-        
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-            public void run() {
-                if (System.currentTimeMillis() - emitTime > (2+emitCount.get())*resizeUpStabilizationDelay) {
-                    //first one may not have been received, in a registration race 
-                    resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, need4);
-                    emitCount.incrementAndGet();
-                }
-                assertEquals(resizable.getCurrentSize(), (Integer)4);
-            }});
-        
-        long resizeDelay = System.currentTimeMillis() - emitTime;
-        assertTrue(resizeDelay >= (resizeUpStabilizationDelay-EARLY_RETURN_MS), "resizeDelay="+resizeDelay);
-    }
-
-    @Test(groups="Integration")
-    public void testResizeDownStabilizationDelayIgnoresBlip() throws Exception {
-        long resizeStabilizationDelay = 1000L;
-        Duration minPeriodBetweenExecs = Duration.ZERO;
-        resizable.removePolicy(policy);
-        
-        policy = AutoScalerPolicy.builder()
-                .resizeDownStabilizationDelay(Duration.of(resizeStabilizationDelay, TimeUnit.MILLISECONDS)) 
-                .minPeriodBetweenExecs(minPeriodBetweenExecs)
-                .build();
-        resizable.addPolicy(policy);
-        resizable.resize(2);
-        
-        // Ignores temporary blip
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(2, 1L, 2*10L, 2*20L)); // would shrink to 1
-        Thread.sleep(resizeStabilizationDelay-OVERHEAD_DURATION_MS);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_OK_SENSOR, message(2, 20L, 1*10L, 1*20L)); // but 2 is still adequate
-        
-        assertEquals(resizable.getCurrentSize(), (Integer)2);
-        Asserts.succeedsContinually(MutableMap.of("duration", 2000L), new Runnable() {
-                public void run() {
-                    assertEquals(resizable.sizes, ImmutableList.of(2));
-                }});
-    }
-
-    // FIXME Acceptance -- see comment against testRepeatedResizeUpStabilizationDelayTakesMaxSustainedDesired
-    @Test(groups={"Integration", "Acceptance"}, invocationCount=MANY_TIMES_INVOCATION_COUNT)
-    public void testRepeatedResizeDownStabilizationDelayTakesMinSustainedDesired() throws Throwable {
-        try {
-            testResizeDownStabilizationDelayTakesMinSustainedDesired();
-        } catch (Throwable t) {
-            dumpThreadsEtc();
-            throw t;
-        }
-    }
-    
-    @Test(groups="Integration")
-    public void testResizeDownStabilizationDelayTakesMinSustainedDesired() throws Exception {
-        long resizeDownStabilizationDelay = 1100L;
-        Duration minPeriodBetweenExecs = Duration.ZERO;
-        policy.suspend();
-        resizable.removePolicy(policy);
-        
-        policy = AutoScalerPolicy.builder()
-                .resizeDownStabilizationDelay(Duration.of(resizeDownStabilizationDelay, TimeUnit.MILLISECONDS))
-                .minPeriodBetweenExecs(minPeriodBetweenExecs)
-                .build();
-        resizable.addPolicy(policy);
-        resizable.resize(3);
-        
-        // Will shrink to only the min sustained in this time window
-        // (i.e. to 2 within the first $resizeUpStabilizationDelay milliseconds)
-        Stopwatch stopwatch = Stopwatch.createStarted();
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(3, 1L, 3*10L, 3*20L)); // would shrink to 1
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(3, 20L, 3*10L, 3*20L)); // would shrink to 2
-        Thread.sleep(resizeDownStabilizationDelay-OVERHEAD_DURATION_MS);
-        
-        long postSleepTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
-        
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(3, 1L, 3*10L, 3*20L)); // would shrink to 1
-
-        // Wait for it to reach size 2, and confirm take expected time
-        // TODO This is time sensitive, and sometimes fails in CI with size=1 if we wait for currentSize==2 (presumably GC kicking in?)
-        //      Therefore do strong assertion of currentSize==2 later, so can write out times if it goes wrong.
-        Asserts.succeedsEventually(MutableMap.of("period", 1, "timeout", TIMEOUT_MS), new Runnable() {
-                public void run() {
-                    assertTrue(resizable.getCurrentSize() <= 2, "currentSize="+resizable.getCurrentSize());
-                }});
-        assertEquals(resizable.getCurrentSize(), (Integer)2, 
-                stopwatch.elapsed(TimeUnit.MILLISECONDS)+"ms after first emission; "+(stopwatch.elapsed(TimeUnit.MILLISECONDS)-postSleepTime)+"ms after last");
-        
-        long timeToResizeTo2 = stopwatch.elapsed(TimeUnit.MILLISECONDS);
-        assertTrue(timeToResizeTo2 >= resizeDownStabilizationDelay-EARLY_RETURN_MS &&
-                timeToResizeTo2 <= resizeDownStabilizationDelay+OVERHEAD_DURATION_MS,
-                "Resizing to 2: time="+timeToResizeTo2+"; resizeDownStabilizationDelay="+resizeDownStabilizationDelay);
-
-        // Will then shrink to 1 $resizeUpStabilizationDelay milliseconds after that emission
-        Asserts.succeedsEventually(MutableMap.of("period", 1, "timeout", TIMEOUT_MS), 
-                currentSizeAsserter(resizable, 1));
-        long timeToResizeTo1 = stopwatch.elapsed(TimeUnit.MILLISECONDS) - postSleepTime;
-        
-        assertTrue(timeToResizeTo1 >= resizeDownStabilizationDelay-EARLY_RETURN_MS &&
-                timeToResizeTo1 <= resizeDownStabilizationDelay+OVERHEAD_DURATION_MS,
-                "Resizing to 1: timeToResizeTo1="+timeToResizeTo1+"; timeToResizeTo2="+timeToResizeTo2+"; resizeDownStabilizationDelay="+resizeDownStabilizationDelay);
-    }
-
-    @Test(groups="Integration")
-    public void testResizeDownStabilizationDelayResizesAfterDelay() throws Exception {
-        final long resizeDownStabilizationDelay = 1000L;
-        Duration minPeriodBetweenExecs = Duration.ZERO;
-        resizable.removePolicy(policy);
-        
-        policy = AutoScalerPolicy.builder()
-                .resizeDownStabilizationDelay(Duration.of(resizeDownStabilizationDelay, TimeUnit.MILLISECONDS))
-                .minPeriodBetweenExecs(minPeriodBetweenExecs)
-                .build();
-        resizable.addPolicy(policy);
-        resizable.resize(2);
-        
-        // After suitable delay, grows to desired
-        final long emitTime = System.currentTimeMillis();
-        final Map<String, Object> needJust1 = message(2, 1L, 2*10L, 2*20L);
-        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, needJust1); // would shrink to 1
-        final AtomicInteger emitCount = new AtomicInteger(0);
-        
-        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-                public void run() {
-                    if (System.currentTimeMillis() - emitTime > (2+emitCount.get())*resizeDownStabilizationDelay) {
-                        //first one may not have been received, in a registration race
-                        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, needJust1); // would shrink to 1
-                        emitCount.incrementAndGet();
-                    }
-                    assertEquals(resizable.getCurrentSize(), (Integer)1);
-                }});
-
-        long resizeDelay = System.currentTimeMillis() - emitTime;
-        assertTrue(resizeDelay >= (resizeDownStabilizationDelay-EARLY_RETURN_MS), "resizeDelay="+resizeDelay);
-    }
-
-    Map<String, Object> message(double currentWorkrate, double lowThreshold, double highThreshold) {
-        return message(resizable.getCurrentSize(), currentWorkrate, lowThreshold, highThreshold);
-    }
-    static Map<String, Object> message(int currentSize, double currentWorkrate, double lowThreshold, double highThreshold) {
-        return ImmutableMap.<String,Object>of(
-            AutoScalerPolicy.POOL_CURRENT_SIZE_KEY, currentSize,
-            AutoScalerPolicy.POOL_CURRENT_WORKRATE_KEY, currentWorkrate,
-            AutoScalerPolicy.POOL_LOW_THRESHOLD_KEY, lowThreshold,
-            AutoScalerPolicy.POOL_HIGH_THRESHOLD_KEY, highThreshold);
-    }
-    
-    public static Runnable currentSizeAsserter(final Resizable resizable, final Integer desired) {
-        return new Runnable() {
-            public void run() {
-                assertEquals(resizable.getCurrentSize(), desired);
-            }
-        };
-    }
-    
-    public static void dumpThreadsEtc() {
-        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
-        ThreadInfo[] threads = threadMXBean.dumpAllThreads(true, true);
-        for (ThreadInfo thread : threads) {
-            System.out.println(thread.getThreadName()+" ("+thread.getThreadState()+")");
-            for (StackTraceElement stackTraceElement : thread.getStackTrace()) {
-                System.out.println("\t"+stackTraceElement);
-            }
-        }
-        
-        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
-        MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
-        MemoryUsage nonHeapMemoryUsage = memoryMXBean.getNonHeapMemoryUsage();
-        System.out.println("Memory:");
-        System.out.println("\tHeap: used="+heapMemoryUsage.getUsed()+"; max="+heapMemoryUsage.getMax()+"; init="+heapMemoryUsage.getInit()+"; committed="+heapMemoryUsage.getCommitted());
-        System.out.println("\tNon-heap: used="+nonHeapMemoryUsage.getUsed()+"; max="+nonHeapMemoryUsage.getMax()+"; init="+nonHeapMemoryUsage.getInit()+"; committed="+nonHeapMemoryUsage.getCommitted());
-
-        OperatingSystemMXBean operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean();
-        System.out.println("OS:");
-        System.out.println("\tsysLoadAvg="+operatingSystemMXBean.getSystemLoadAverage()+"; availableProcessors="+operatingSystemMXBean.getAvailableProcessors()+"; arch="+operatingSystemMXBean.getArch());
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/autoscaling/LocallyResizableEntity.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/autoscaling/LocallyResizableEntity.java b/policy/src/test/java/brooklyn/policy/autoscaling/LocallyResizableEntity.java
deleted file mode 100644
index c6b3c09..0000000
--- a/policy/src/test/java/brooklyn/policy/autoscaling/LocallyResizableEntity.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.autoscaling;
-
-import java.util.List;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.test.entity.TestCluster;
-
-import brooklyn.entity.basic.AbstractEntity;
-import brooklyn.entity.trait.Resizable;
-import brooklyn.entity.trait.Startable;
-
-import com.google.common.base.Throwables;
-import com.google.common.collect.Lists;
-
-/**
- * Test class for providing a Resizable LocallyManagedEntity for policy testing
- * It is hooked up to a TestCluster that can be used to make assertions against
- */
-public class LocallyResizableEntity extends AbstractEntity implements Resizable {
-    List<Integer> sizes = Lists.newArrayList();
-    TestCluster cluster;
-    long resizeSleepTime = 0;
-    
-    public LocallyResizableEntity (TestCluster tc) {
-        this(null, tc);
-    }
-    @SuppressWarnings("deprecation")
-    public LocallyResizableEntity (Entity parent, TestCluster tc) {
-        super(parent);
-        this.cluster = tc;
-        setAttribute(Startable.SERVICE_UP, true);
-    }
-    
-    @Override
-    public Integer resize(Integer newSize) {
-        try {
-            Thread.sleep(resizeSleepTime);
-            sizes.add(newSize); 
-            return cluster.resize(newSize);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw Throwables.propagate(e);
-        }
-    }
-    
-    @Override
-    public Integer getCurrentSize() {
-        return cluster.getCurrentSize();
-    }
-    
-    @Override
-    public String toString() {
-        return getDisplayName();
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/followthesun/AbstractFollowTheSunPolicyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/followthesun/AbstractFollowTheSunPolicyTest.java b/policy/src/test/java/brooklyn/policy/followthesun/AbstractFollowTheSunPolicyTest.java
deleted file mode 100644
index c8855f2..0000000
--- a/policy/src/test/java/brooklyn/policy/followthesun/AbstractFollowTheSunPolicyTest.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import static org.testng.Assert.assertEquals;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.Random;
-import java.util.Set;
-
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.Group;
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.entity.proxying.EntitySpec;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.api.location.LocationSpec;
-import org.apache.brooklyn.api.management.ManagementContext;
-import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
-import org.apache.brooklyn.test.entity.TestApplication;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-
-import brooklyn.entity.basic.ApplicationBuilder;
-import brooklyn.entity.basic.DynamicGroup;
-import brooklyn.entity.basic.Entities;
-
-import org.apache.brooklyn.location.basic.SimulatedLocation;
-
-import brooklyn.policy.loadbalancing.BalanceableContainer;
-import brooklyn.policy.loadbalancing.MockContainerEntity;
-import brooklyn.policy.loadbalancing.MockItemEntity;
-import brooklyn.policy.loadbalancing.MockItemEntityImpl;
-import brooklyn.policy.loadbalancing.Movable;
-import brooklyn.test.Asserts;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.time.Time;
-
-import com.google.common.base.Function;
-import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-
-public class AbstractFollowTheSunPolicyTest {
-    
-    private static final Logger LOG = LoggerFactory.getLogger(AbstractFollowTheSunPolicyTest.class);
-    
-    protected static final long TIMEOUT_MS = 10*1000;
-    protected static final long SHORT_WAIT_MS = 250;
-    
-    protected static final long CONTAINER_STARTUP_DELAY_MS = 100;
-    
-    protected TestApplication app;
-    protected ManagementContext managementContext;
-    protected SimulatedLocation loc1;
-    protected SimulatedLocation loc2;
-    protected FollowTheSunPool pool;
-    protected DefaultFollowTheSunModel<Entity, Movable> model;
-    protected FollowTheSunPolicy policy;
-    protected Group containerGroup;
-    protected Group itemGroup;
-    protected Random random = new Random();
-    
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        LOG.debug("In AbstractFollowTheSunPolicyTest.setUp()");
-
-        MockItemEntityImpl.totalMoveCount.set(0);
-        MockItemEntityImpl.lastMoveTime.set(0);
-        
-        managementContext = LocalManagementContextForTests.newInstance();
-        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
-        
-        loc1 = managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class).configure("name", "loc1"));
-        loc2 = managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class).configure("name", "loc2"));
-        
-        containerGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
-                .displayName("containerGroup")
-                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockContainerEntity.class)));
-        
-        itemGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
-                .displayName("itemGroup")
-                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockItemEntity.class)));
-        model = new DefaultFollowTheSunModel<Entity, Movable>("pool-model");
-        pool = app.createAndManageChild(EntitySpec.create(FollowTheSunPool.class));
-        pool.setContents(containerGroup, itemGroup);
-        policy = new FollowTheSunPolicy(MockItemEntity.ITEM_USAGE_METRIC, model, FollowTheSunParameters.newDefault());
-        pool.addPolicy(policy);
-        app.start(ImmutableList.of(loc1, loc2));
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() {
-        if (pool != null && policy != null) pool.removePolicy(policy);
-        if (app != null) Entities.destroyAll(app.getManagementContext());
-        MockItemEntityImpl.totalMoveCount.set(0);
-        MockItemEntityImpl.lastMoveTime.set(0);
-    }
-    
-    /**
-     * Asserts that the given container have the given expected workrates (by querying the containers directly).
-     * Accepts an accuracy of "precision" for each container's workrate.
-     */
-    protected void assertItemDistributionEventually(final Map<MockContainerEntity, ? extends Collection<MockItemEntity>> expected) {
-        try {
-            Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
-                public void run() {
-                    assertItemDistribution(expected);
-                }});
-        } catch (AssertionError e) {
-            String errMsg = e.getMessage()+"; "+verboseDumpToString();
-            throw new RuntimeException(errMsg, e);
-        }
-    }
-
-    protected void assertItemDistributionContinually(final Map<MockContainerEntity, Collection<MockItemEntity>> expected) {
-        try {
-            Asserts.succeedsContinually(MutableMap.of("timeout", SHORT_WAIT_MS), new Runnable() {
-                public void run() {
-                    assertItemDistribution(expected);
-                }});
-        } catch (AssertionError e) {
-            String errMsg = e.getMessage()+"; "+verboseDumpToString();
-            throw new RuntimeException(errMsg, e);
-        }
-    }
-
-    protected void assertItemDistribution(Map<MockContainerEntity, ? extends Collection<MockItemEntity>> expected) {
-        String errMsg = verboseDumpToString();
-        for (Map.Entry<MockContainerEntity, ? extends Collection<MockItemEntity>> entry : expected.entrySet()) {
-            MockContainerEntity container = entry.getKey();
-            Collection<MockItemEntity> expectedItems = entry.getValue();
-            
-            assertEquals(ImmutableSet.copyOf(container.getBalanceableItems()), ImmutableSet.copyOf(expectedItems), errMsg);
-        }
-    }
-
-    protected String verboseDumpToString() {
-        Iterable<MockContainerEntity> containers = Iterables.filter(app.getManagementContext().getEntityManager().getEntities(), MockContainerEntity.class);
-        Iterable<MockItemEntity> items = Iterables.filter(app.getManagementContext().getEntityManager().getEntities(), MockItemEntity.class);
-        
-        Iterable<Double> containerRates = Iterables.transform(containers, new Function<MockContainerEntity, Double>() {
-            @Override public Double apply(MockContainerEntity input) {
-                return (double) input.getWorkrate();
-            }});
-        
-        Iterable<Map<Entity, Double>> containerItemUsages = Iterables.transform(containers, new Function<MockContainerEntity, Map<Entity, Double>>() {
-            @Override public Map<Entity, Double> apply(MockContainerEntity input) {
-                return input.getItemUsage();
-            }});
-        
-        Map<MockContainerEntity, Set<Movable>> itemDistributionByContainer = Maps.newLinkedHashMap();
-        for (MockContainerEntity container : containers) {
-            itemDistributionByContainer.put(container, container.getBalanceableItems());
-        }
-        
-        Map<Movable, BalanceableContainer<?>> itemDistributionByItem = Maps.newLinkedHashMap();
-        for (Movable item : items) {
-            itemDistributionByItem.put(item, item.getAttribute(Movable.CONTAINER));
-        }
-
-        String modelItemDistribution = model.itemDistributionToString();
-        
-        return "containers="+containers+"; containerRates="+containerRates
-                +"; containerItemUsages="+containerItemUsages
-                +"; itemDistributionByContainer="+itemDistributionByContainer
-                +"; itemDistributionByItem="+itemDistributionByItem
-                +"; model="+modelItemDistribution
-                +"; totalMoves="+MockItemEntityImpl.totalMoveCount
-                +"; lastMoveTime="+Time.makeDateString(MockItemEntityImpl.lastMoveTime.get());
-    }
-    
-    protected MockContainerEntity newContainer(TestApplication app, Location loc, String name) {
-        return newAsyncContainer(app, loc, name, 0);
-    }
-    
-    /**
-     * Creates a new container that will take "delay" millis to complete its start-up.
-     */
-    protected MockContainerEntity newAsyncContainer(TestApplication app, Location loc, String name, long delay) {
-        // FIXME Is this comment true?
-        // Annoyingly, can't set parent until after the threshold config has been defined.
-        MockContainerEntity container = app.createAndManageChild(EntitySpec.create(MockContainerEntity.class)
-                .displayName(name)
-                .configure(MockContainerEntity.DELAY, delay));
-        LOG.debug("Managed new container {}", container);
-        container.start(ImmutableList.of(loc));
-        return container;
-    }
-
-    protected static MockItemEntity newLockedItem(TestApplication app, MockContainerEntity container, String name) {
-        MockItemEntity item = app.createAndManageChild(EntitySpec.create(MockItemEntity.class)
-                .displayName(name)
-                .configure(MockItemEntity.IMMOVABLE, true));
-        LOG.debug("Managed new locked item {}", container);
-        if (container != null) {
-            item.move(container);
-        }
-        return item;
-    }
-    
-    protected static MockItemEntity newItem(TestApplication app, MockContainerEntity container, String name) {
-        MockItemEntity item = app.createAndManageChild(EntitySpec.create(MockItemEntity.class)
-                .displayName(name));
-        LOG.debug("Managed new item {} at {}", item, container);
-        if (container != null) {
-            item.move(container);
-        }
-        return item;
-    }
-    
-    protected static MockItemEntity newItem(TestApplication app, MockContainerEntity container, String name, Map<? extends Entity, Double> workpattern) {
-        MockItemEntity item = newItem(app, container, name);
-        if (workpattern != null) {
-            ((EntityLocal)item).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, (Map) workpattern);
-        }
-        return item;
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunModelTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunModelTest.java b/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunModelTest.java
deleted file mode 100644
index 9b16a2a..0000000
--- a/policy/src/test/java/brooklyn/policy/followthesun/FollowTheSunModelTest.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.followthesun;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertTrue;
-
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-import org.apache.brooklyn.api.location.Location;
-import org.apache.brooklyn.location.basic.SimulatedLocation;
-
-import brooklyn.policy.loadbalancing.MockContainerEntity;
-import brooklyn.policy.loadbalancing.MockContainerEntityImpl;
-import brooklyn.policy.loadbalancing.MockItemEntity;
-import brooklyn.policy.loadbalancing.MockItemEntityImpl;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-
-public class FollowTheSunModelTest {
-
-    private Location loc1 = new SimulatedLocation(DefaultFollowTheSunModel.newHashMap("name","loc1"));
-    private Location loc2 = new SimulatedLocation(DefaultFollowTheSunModel.newHashMap("name","loc2"));
-    private MockContainerEntity container1 = new MockContainerEntityImpl();
-    private MockContainerEntity container2 = new MockContainerEntityImpl();
-    private MockItemEntity item1 = new MockItemEntityImpl();
-    private MockItemEntity item2 = new MockItemEntityImpl();
-    private MockItemEntity item3 = new MockItemEntityImpl();
-    
-    private DefaultFollowTheSunModel<MockContainerEntity, MockItemEntity> model;
-
-    @BeforeMethod(alwaysRun=true)
-    public void setUp() throws Exception {
-        model = new DefaultFollowTheSunModel<MockContainerEntity, MockItemEntity>("myname");
-    }
-    
-    @AfterMethod(alwaysRun=true)
-    public void tearDown() throws Exception {
-        // noting to tear down; no management context created
-    }
-
-    @Test
-    public void testSimpleAddAndRemove() throws Exception {
-        model.onContainerAdded(container1, loc1);
-        model.onContainerAdded(container2, loc2);
-        model.onItemAdded(item1, container1, true);
-        model.onItemAdded(item2, container2, true);
-        
-        assertEquals(model.getContainerLocation(container1), loc1);
-        assertEquals(model.getContainerLocation(container2), loc2);
-        assertEquals(model.getItems(), ImmutableSet.of(item1, item2));
-        assertEquals(model.getItemLocation(item1), loc1);
-        assertEquals(model.getItemLocation(item2), loc2);
-        assertEquals(model.getItemContainer(item1), container1);
-        assertEquals(model.getItemContainer(item2), container2);
-        
-        model.onContainerRemoved(container2);
-        model.onItemRemoved(item2);
-        
-        assertEquals(model.getContainerLocation(container1), loc1);
-        assertEquals(model.getContainerLocation(container2), null);
-        assertEquals(model.getItems(), ImmutableSet.of(item1));
-        assertEquals(model.getItemLocation(item1), loc1);
-        assertEquals(model.getItemLocation(item2), null);
-        assertEquals(model.getItemContainer(item1), container1);
-        assertEquals(model.getItemContainer(item2), null);
-    }
-    
-    @Test
-    public void testItemUsageMetrics() throws Exception {
-        model.onContainerAdded(container1, loc1);
-        model.onContainerAdded(container2, loc2);
-        model.onItemAdded(item1, container1, true);
-        model.onItemAdded(item2, container2, true);
-
-        model.onItemUsageUpdated(item1, ImmutableMap.of(item2, 12d));
-        model.onItemUsageUpdated(item2, ImmutableMap.of(item1, 11d));
-        
-        assertEquals(model.getDirectSendsToItemByLocation(),
-                ImmutableMap.of(item1, ImmutableMap.of(loc2, 12d), item2, ImmutableMap.of(loc1, 11d)));
-    }
-    
-    @Test
-    public void testItemUsageReportedIfLocationSetAfterUsageUpdate() throws Exception {
-        model.onContainerAdded(container1, null);
-        model.onContainerAdded(container2, null);
-        model.onItemAdded(item1, container1, true);
-        model.onItemAdded(item2, container2, true);
-        model.onItemUsageUpdated(item1, ImmutableMap.of(item2, 12d));
-        model.onContainerLocationUpdated(container1, loc1);
-        model.onContainerLocationUpdated(container2, loc2);
-        
-        assertEquals(model.getDirectSendsToItemByLocation(),
-                ImmutableMap.of(item1, ImmutableMap.of(loc2, 12d)));
-    }
-    
-    @Test
-    public void testItemUsageMetricsSummedForActorsInSameLocation() throws Exception {
-        model.onContainerAdded(container1, loc1);
-        model.onContainerAdded(container2, loc2);
-        model.onItemAdded(item1, container1, true);
-        model.onItemAdded(item2, container2, true);
-        model.onItemAdded(item3, container2, true);
-
-        model.onItemUsageUpdated(item1, ImmutableMap.of(item2, 12d, item3, 13d));
-        
-        assertEquals(model.getDirectSendsToItemByLocation(),
-                ImmutableMap.of(item1, ImmutableMap.of(loc2, 12d+13d)));
-    }
-    
-    @Test
-    public void testItemMovedWillUpdateLocationUsage() throws Exception {
-        model.onContainerAdded(container1, loc1);
-        model.onContainerAdded(container2, loc2);
-        model.onItemAdded(item1, container1, false);
-        model.onItemAdded(item2, container2, false);
-        model.onItemUsageUpdated(item2, ImmutableMap.of(item1, 12d));
-        
-        model.onItemMoved(item1, container2);
-
-        assertEquals(model.getDirectSendsToItemByLocation(),
-                ImmutableMap.of(item2, ImmutableMap.of(loc2, 12d)));
-        assertEquals(model.getItemContainer(item1), container2);
-        assertEquals(model.getItemLocation(item1), loc2);
-    }
-    
-    @Test
-    public void testItemAddedWithNoContainer() throws Exception {
-        model.onItemAdded(item1, null, true);
-
-        assertEquals(model.getItems(), ImmutableSet.of(item1));
-        assertEquals(model.getItemContainer(item1), null);
-        assertEquals(model.getItemLocation(item1), null);
-    }
-    
-    @Test
-    public void testItemAddedBeforeContainer() throws Exception {
-        model.onItemAdded(item1, container1, true);
-        model.onContainerAdded(container1, loc1);
-
-        assertEquals(model.getItems(), ImmutableSet.of(item1));
-        assertEquals(model.getItemContainer(item1), container1);
-        assertEquals(model.getItemLocation(item1), loc1);
-    }
-    
-    @Test
-    public void testItemMovedBeforeContainerAdded() throws Exception {
-        model.onContainerAdded(container1, loc1);
-        model.onItemAdded(item1, container1, true);
-        model.onItemMoved(item1, container2);
-        model.onContainerAdded(container2, loc2);
-
-        assertEquals(model.getItems(), ImmutableSet.of(item1));
-        assertEquals(model.getItemContainer(item1), container2);
-        assertEquals(model.getItemLocation(item1), loc2);
-    }
-    
-    @Test
-    public void testItemAddedAnswersMovability() throws Exception {
-        model.onItemAdded(item1, container1, false);
-        model.onItemAdded(item2, container1, true);
-        assertTrue(model.isItemMoveable(item1));
-        assertFalse(model.isItemMoveable(item2));
-    }
-    
-    @Test
-    public void testWorkrateUpdateAfterItemRemovalIsNotRecorded() throws Exception {
-        model.onContainerAdded(container1, loc1);
-        model.onItemAdded(item1, container1, true);
-        model.onItemAdded(item2, container1, true);
-        model.onItemRemoved(item1);
-        model.onItemUsageUpdated(item1, ImmutableMap.of(item2, 123d));
-        
-        assertFalse(model.getDirectSendsToItemByLocation().containsKey(item1));
-    }
-}


[04/24] incubator-brooklyn git commit: Merge branch 'master' of github.com:apache/incubator-brooklyn into add-postgres-user

Posted by he...@apache.org.
Merge branch 'master' of github.com:apache/incubator-brooklyn into add-postgres-user

Conflicts:
	software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java


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

Branch: refs/heads/master
Commit: 8cba4d3cd437f5e59be1bc24bbd875eaee10ff20
Parents: ac82d23 5dfe944
Author: Robert Moss <ro...@gmail.com>
Authored: Tue Aug 18 10:26:54 2015 +0100
Committer: Robert Moss <ro...@gmail.com>
Committed: Tue Aug 18 10:26:54 2015 +0100

----------------------------------------------------------------------
 LICENSE                                         |  275 ++-
 .../src/main/java/brooklyn/BrooklynVersion.java |    6 +-
 .../brooklyn/basic/AbstractBrooklynObject.java  |  249 ---
 .../brooklyn/basic/BasicConfigurableObject.java |  120 --
 .../brooklyn/basic/BrooklynDynamicType.java     |  284 ---
 .../brooklyn/basic/BrooklynObjectInternal.java  |  104 -
 .../brooklyn/basic/BrooklynTypeSnapshot.java    |  102 -
 .../main/java/brooklyn/basic/BrooklynTypes.java |  132 --
 .../basic/internal/ApiObjectsFactoryImpl.java   |   42 -
 .../brooklyn/config/BrooklynProperties.java     |    6 +-
 .../brooklyn/config/BrooklynServerPaths.java    |    2 +-
 .../config/internal/AbstractConfigMapImpl.java  |    4 +-
 .../enricher/CustomAggregatingEnricher.java     |    2 +-
 .../main/java/brooklyn/enricher/Enrichers.java  |    2 +-
 .../enricher/basic/AbstractEnricher.java        |    4 +-
 .../basic/AbstractMultipleSensorAggregator.java |    2 +-
 .../basic/AbstractTypeTransformingEnricher.java |    2 +-
 .../brooklyn/enricher/basic/Aggregator.java     |    2 +-
 .../enricher/basic/EnricherDynamicType.java     |    2 +-
 .../enricher/basic/EnricherTypeSnapshot.java    |    2 +-
 .../java/brooklyn/enricher/basic/Joiner.java    |    2 +-
 .../brooklyn/enricher/basic/Propagator.java     |    6 +-
 .../brooklyn/enricher/basic/Transformer.java    |    4 +-
 .../brooklyn/enricher/basic/UpdatingMap.java    |    2 +-
 .../basic/YamlTimeWeightedDeltaEnricher.java    |    2 +-
 .../brooklyn/entity/basic/AbstractEffector.java |    4 +-
 .../brooklyn/entity/basic/AbstractEntity.java   |   16 +-
 .../java/brooklyn/entity/basic/BasicGroup.java  |    2 +-
 .../entity/basic/BrooklynConfigKeys.java        |    4 +-
 .../entity/basic/BrooklynShutdownHooks.java     |    2 +-
 .../brooklyn/entity/basic/BrooklynTaskTags.java |    6 +-
 .../java/brooklyn/entity/basic/ConfigKeys.java  |    2 +-
 .../java/brooklyn/entity/basic/DataEntity.java  |    2 +-
 .../brooklyn/entity/basic/DynamicGroup.java     |    2 +-
 .../brooklyn/entity/basic/DynamicGroupImpl.java |    2 +-
 .../entity/basic/EffectorStartableImpl.java     |    3 +-
 .../java/brooklyn/entity/basic/Entities.java    |   20 +-
 .../brooklyn/entity/basic/EntityConfigMap.java  |   10 +-
 .../entity/basic/EntityDynamicType.java         |    2 +-
 .../brooklyn/entity/basic/EntityFunctions.java  |    2 +-
 .../brooklyn/entity/basic/EntityInternal.java   |    4 +-
 .../basic/EntityTransientCopyInternal.java      |    2 +-
 .../entity/basic/EntityTypeSnapshot.java        |    2 +-
 .../java/brooklyn/entity/basic/EntityTypes.java |    2 +-
 .../java/brooklyn/entity/basic/Lifecycle.java   |    2 +-
 .../brooklyn/entity/basic/MethodEffector.java   |    2 +-
 .../java/brooklyn/entity/basic/Sanitizer.java   |    2 +-
 .../entity/basic/ServiceStateLogic.java         |    2 +-
 .../entity/effector/AddChildrenEffector.java    |    2 +-
 .../brooklyn/entity/effector/AddEffector.java   |    2 +-
 .../brooklyn/entity/effector/AddSensor.java     |    2 +-
 .../brooklyn/entity/effector/EffectorBody.java  |   10 +-
 .../brooklyn/entity/effector/EffectorTasks.java |   10 +-
 .../brooklyn/entity/effector/Effectors.java     |    4 +-
 .../group/AbstractMembershipTrackingPolicy.java |    2 +-
 .../java/brooklyn/entity/group/Cluster.java     |    2 +-
 .../brooklyn/entity/group/DynamicCluster.java   |    2 +-
 .../entity/group/DynamicClusterImpl.java        |    8 +-
 .../brooklyn/entity/group/DynamicFabric.java    |    2 +-
 .../entity/group/DynamicMultiGroup.java         |    2 +-
 .../entity/group/QuarantineGroupImpl.java       |    4 +-
 .../entity/proxying/EntityProxyImpl.java        |    6 +-
 .../entity/proxying/InternalEntityFactory.java  |    6 +-
 .../proxying/InternalLocationFactory.java       |    4 +-
 .../entity/proxying/InternalPolicyFactory.java  |    4 +-
 .../AbstractBrooklynObjectRebindSupport.java    |    4 +-
 .../rebind/ActivePartialRebindIteration.java    |    2 +-
 .../rebind/BasicCatalogItemRebindSupport.java   |    2 +-
 .../rebind/BasicEnricherRebindSupport.java      |    4 +-
 .../entity/rebind/BasicEntityRebindSupport.java |    2 +-
 .../entity/rebind/BasicFeedRebindSupport.java   |    4 +-
 .../rebind/BasicLocationRebindSupport.java      |    4 +-
 .../entity/rebind/BasicPolicyRebindSupport.java |    7 +-
 .../rebind/ImmediateDeltaChangeListener.java    |    2 +-
 .../rebind/PeriodicDeltaChangeListener.java     |    6 +-
 .../brooklyn/entity/rebind/RebindIteration.java |    8 +-
 .../entity/rebind/RebindManagerImpl.java        |    6 +-
 .../entity/rebind/dto/BasicEntityMemento.java   |    3 +-
 .../entity/rebind/dto/BasicLocationMemento.java |    2 +-
 .../entity/rebind/dto/MementosGenerators.java   |    8 +-
 .../BrooklynMementoPersisterToObjectStore.java  |    2 +-
 .../persister/BrooklynPersistenceUtils.java     |    4 +-
 .../rebind/persister/FileBasedObjectStore.java  |    2 +-
 .../rebind/persister/XmlMementoSerializer.java  |    2 +-
 .../rebind/transformer/CompoundTransformer.java |    4 +-
 .../transformer/CompoundTransformerLoader.java  |    4 +-
 .../java/brooklyn/entity/trait/Startable.java   |    4 +-
 .../brooklyn/entity/trait/StartableMethods.java |    6 +-
 .../java/brooklyn/event/basic/AttributeMap.java |    2 +-
 .../basic/AttributeSensorAndConfigKey.java      |    2 +-
 .../brooklyn/event/basic/BasicConfigKey.java    |    4 +-
 .../event/basic/DependentConfiguration.java     |   16 +-
 .../basic/PortAttributeSensorAndConfigKey.java  |    2 +-
 ...platedStringAttributeSensorAndConfigKey.java |    2 +-
 .../java/brooklyn/event/feed/AbstractFeed.java  |    2 +-
 .../event/feed/AttributePollHandler.java        |    4 +-
 .../main/java/brooklyn/event/feed/Poller.java   |    6 +-
 .../java/brooklyn/event/feed/http/HttpFeed.java |    6 +-
 .../event/feed/http/HttpPollConfig.java         |    4 +-
 .../brooklyn/event/feed/http/HttpPollValue.java |    2 +-
 .../brooklyn/event/feed/http/HttpPolls.java     |    5 +-
 .../event/feed/http/HttpValueFunctions.java     |    3 +-
 .../brooklyn/event/feed/shell/ShellFeed.java    |    6 +-
 .../java/brooklyn/event/feed/ssh/SshFeed.java   |    6 +-
 .../windows/WindowsPerformanceCounterFeed.java  |    4 +-
 .../policy/basic/AbstractEntityAdjunct.java     |  510 -----
 .../brooklyn/policy/basic/AbstractPolicy.java   |  119 --
 .../java/brooklyn/policy/basic/AdjunctType.java |  174 --
 .../brooklyn/policy/basic/ConfigMapImpl.java    |  140 --
 .../policy/basic/GeneralPurposePolicy.java      |   36 -
 .../java/brooklyn/policy/basic/Policies.java    |   73 -
 .../policy/basic/PolicyDynamicType.java         |   44 -
 .../policy/basic/PolicyTypeSnapshot.java        |   40 -
 .../util/BrooklynLanguageExtensions.java        |   48 -
 .../brooklyn/util/BrooklynMavenArtifacts.java   |   58 -
 .../brooklyn/util/BrooklynNetworkUtils.java     |   41 -
 .../main/java/brooklyn/util/ResourceUtils.java  |  639 ------
 .../java/brooklyn/util/config/ConfigBag.java    |  588 ------
 .../brooklyn/util/crypto/FluentKeySigner.java   |  192 --
 .../java/brooklyn/util/crypto/SecureKeys.java   |  184 --
 .../java/brooklyn/util/file/ArchiveBuilder.java |  423 ----
 .../java/brooklyn/util/file/ArchiveTasks.java   |   58 -
 .../java/brooklyn/util/file/ArchiveUtils.java   |  351 ----
 .../util/flags/ClassCoercionException.java      |   39 -
 .../java/brooklyn/util/flags/FlagUtils.java     |  587 ------
 .../brooklyn/util/flags/MethodCoercions.java    |  183 --
 .../java/brooklyn/util/flags/SetFromFlag.java   |   71 -
 .../java/brooklyn/util/flags/TypeCoercions.java |  879 --------
 .../main/java/brooklyn/util/http/HttpTool.java  |  387 ----
 .../brooklyn/util/http/HttpToolResponse.java    |  185 --
 .../util/internal/ConfigKeySelfExtracting.java  |   41 -
 .../java/brooklyn/util/internal/Repeater.java   |  369 ----
 .../ssh/BackoffLimitedRetryHandler.java         |   74 -
 .../util/internal/ssh/ShellAbstractTool.java    |  442 ----
 .../brooklyn/util/internal/ssh/ShellTool.java   |  113 -
 .../util/internal/ssh/SshAbstractTool.java      |  172 --
 .../util/internal/ssh/SshException.java         |   32 -
 .../brooklyn/util/internal/ssh/SshTool.java     |  174 --
 .../util/internal/ssh/cli/SshCliTool.java       |  316 ---
 .../util/internal/ssh/process/ProcessTool.java  |  214 --
 .../internal/ssh/sshj/SshjClientConnection.java |  282 ---
 .../util/internal/ssh/sshj/SshjTool.java        | 1091 ----------
 .../util/javalang/ReflectionScanner.java        |  135 --
 .../brooklyn/util/javalang/UrlClassLoader.java  |   69 -
 .../java/brooklyn/util/mutex/MutexSupport.java  |  120 --
 .../brooklyn/util/mutex/SemaphoreForTasks.java  |  112 -
 .../util/mutex/SemaphoreWithOwners.java         |  231 --
 .../java/brooklyn/util/mutex/WithMutexes.java   |   45 -
 .../src/main/java/brooklyn/util/osgi/Osgis.java |  719 -------
 .../util/task/AbstractExecutionContext.java     |   75 -
 .../util/task/BasicExecutionContext.java        |  221 --
 .../util/task/BasicExecutionManager.java        |  755 -------
 .../main/java/brooklyn/util/task/BasicTask.java |  892 --------
 .../java/brooklyn/util/task/CanSetName.java     |   25 -
 .../java/brooklyn/util/task/CompoundTask.java   |  131 --
 .../brooklyn/util/task/DeferredSupplier.java    |   38 -
 .../util/task/DynamicSequentialTask.java        |  480 -----
 .../java/brooklyn/util/task/DynamicTasks.java   |  337 ---
 .../brooklyn/util/task/ExecutionListener.java   |   31 -
 .../java/brooklyn/util/task/ExecutionUtils.java |   49 -
 .../java/brooklyn/util/task/ForwardingTask.java |  325 ---
 .../util/task/ListenableForwardingFuture.java   |   50 -
 .../java/brooklyn/util/task/ParallelTask.java   |   85 -
 .../java/brooklyn/util/task/ScheduledTask.java  |  185 --
 .../java/brooklyn/util/task/SequentialTask.java |   58 -
 .../util/task/SingleThreadedScheduler.java      |  216 --
 .../java/brooklyn/util/task/TaskBuilder.java    |  184 --
 .../java/brooklyn/util/task/TaskInternal.java   |  125 --
 .../java/brooklyn/util/task/TaskScheduler.java  |   41 -
 .../main/java/brooklyn/util/task/TaskTags.java  |   71 -
 .../src/main/java/brooklyn/util/task/Tasks.java |  488 -----
 .../java/brooklyn/util/task/ValueResolver.java  |  426 ----
 .../util/task/ssh/SshFetchTaskFactory.java      |   89 -
 .../util/task/ssh/SshFetchTaskWrapper.java      |  135 --
 .../util/task/ssh/SshPutTaskFactory.java        |  123 --
 .../brooklyn/util/task/ssh/SshPutTaskStub.java  |   69 -
 .../util/task/ssh/SshPutTaskWrapper.java        |  190 --
 .../java/brooklyn/util/task/ssh/SshTasks.java   |  236 ---
 .../internal/AbstractSshExecTaskFactory.java    |   58 -
 .../ssh/internal/PlainSshExecTaskFactory.java   |   71 -
 .../util/task/system/ProcessTaskFactory.java    |   65 -
 .../util/task/system/ProcessTaskStub.java       |  101 -
 .../util/task/system/ProcessTaskWrapper.java    |  187 --
 .../brooklyn/util/task/system/SystemTasks.java  |   29 -
 .../internal/AbstractProcessTaskFactory.java    |  214 --
 .../system/internal/ExecWithLoggingHelpers.java |  200 --
 .../internal/SystemProcessTaskFactory.java      |  131 --
 .../brooklyn/util/text/DataUriSchemeParser.java |  267 ---
 .../brooklyn/util/text/TemplateProcessor.java   |  397 ----
 ...ompilerIndependentOuterClassFieldMapper.java |  166 --
 .../xstream/EnumCaseForgivingConverter.java     |   60 -
 .../EnumCaseForgivingSingleValueConverter.java  |   35 -
 .../util/xstream/ImmutableListConverter.java    |   54 -
 .../util/xstream/ImmutableMapConverter.java     |   56 -
 .../util/xstream/ImmutableSetConverter.java     |   54 -
 .../util/xstream/Inet4AddressConverter.java     |   65 -
 .../brooklyn/util/xstream/MapConverter.java     |  104 -
 .../util/xstream/MutableSetConverter.java       |   44 -
 .../util/xstream/StringKeyMapConverter.java     |  134 --
 .../brooklyn/util/xstream/XmlSerializer.java    |   97 -
 .../java/brooklyn/util/xstream/XmlUtil.java     |   59 -
 .../brooklyn/basic/AbstractBrooklynObject.java  |  249 +++
 .../brooklyn/basic/BasicConfigurableObject.java |  120 ++
 .../brooklyn/basic/BrooklynDynamicType.java     |  284 +++
 .../brooklyn/basic/BrooklynObjectInternal.java  |  104 +
 .../brooklyn/basic/BrooklynTypeSnapshot.java    |  102 +
 .../apache/brooklyn/basic/BrooklynTypes.java    |  132 ++
 .../basic/internal/ApiObjectsFactoryImpl.java   |   42 +
 .../catalog/internal/BasicBrooklynCatalog.java  |    4 +-
 .../catalog/internal/CatalogClasspathDo.java    |    6 +-
 .../core/catalog/internal/CatalogDto.java       |    2 +-
 .../core/catalog/internal/CatalogDtoUtils.java  |    2 +-
 .../catalog/internal/CatalogInitialization.java |    4 +-
 .../core/catalog/internal/CatalogItemDo.java    |    2 +-
 .../internal/CatalogItemDtoAbstract.java        |    6 +-
 .../core/catalog/internal/CatalogUtils.java     |    2 +-
 .../catalog/internal/CatalogXmlSerializer.java  |    8 +-
 .../internal/BrooklynFeatureEnablement.java     |    2 +-
 .../core/internal/BrooklynInitialization.java   |    9 +-
 .../management/entitlement/Entitlements.java    |    2 +-
 .../ha/HighAvailabilityManagerImpl.java         |    4 +-
 .../core/management/ha/OsgiManager.java         |    4 +-
 .../internal/AbstractManagementContext.java     |    8 +-
 .../internal/AsyncCollectionChangeAdapter.java  |    4 +-
 .../internal/BrooklynGarbageCollector.java      |    6 +-
 .../core/management/internal/EffectorUtils.java |    4 +-
 .../internal/EntityManagementUtils.java         |    4 +-
 .../management/internal/LocalEntityManager.java |    2 +-
 .../internal/LocalLocationManager.java          |    4 +-
 .../internal/LocalManagementContext.java        |   10 +-
 .../internal/LocalSubscriptionManager.java      |    4 +-
 .../management/internal/LocalUsageManager.java  |    2 +-
 .../internal/ManagementContextInternal.java     |    2 +-
 .../policy/basic/AbstractEntityAdjunct.java     |  510 +++++
 .../core/policy/basic/AbstractPolicy.java       |  119 ++
 .../brooklyn/core/policy/basic/AdjunctType.java |  174 ++
 .../core/policy/basic/ConfigMapImpl.java        |  140 ++
 .../core/policy/basic/GeneralPurposePolicy.java |   36 +
 .../brooklyn/core/policy/basic/Policies.java    |   73 +
 .../core/policy/basic/PolicyDynamicType.java    |   44 +
 .../core/policy/basic/PolicyTypeSnapshot.java   |   40 +
 .../core/util/BrooklynLanguageExtensions.java   |   48 +
 .../core/util/BrooklynMavenArtifacts.java       |   58 +
 .../core/util/BrooklynNetworkUtils.java         |   44 +
 .../brooklyn/core/util/ResourceUtils.java       |  639 ++++++
 .../brooklyn/core/util/config/ConfigBag.java    |  589 ++++++
 .../core/util/crypto/FluentKeySigner.java       |  192 ++
 .../brooklyn/core/util/crypto/SecureKeys.java   |  186 ++
 .../brooklyn/core/util/file/ArchiveBuilder.java |  424 ++++
 .../brooklyn/core/util/file/ArchiveTasks.java   |   58 +
 .../brooklyn/core/util/file/ArchiveUtils.java   |  351 ++++
 .../core/util/flags/ClassCoercionException.java |   39 +
 .../brooklyn/core/util/flags/FlagUtils.java     |  587 ++++++
 .../core/util/flags/MethodCoercions.java        |  183 ++
 .../brooklyn/core/util/flags/SetFromFlag.java   |   71 +
 .../brooklyn/core/util/flags/TypeCoercions.java |  879 ++++++++
 .../brooklyn/core/util/http/HttpTool.java       |  387 ++++
 .../core/util/http/HttpToolResponse.java        |  185 ++
 .../util/internal/ConfigKeySelfExtracting.java  |   41 +
 .../brooklyn/core/util/internal/Repeater.java   |  370 ++++
 .../ssh/BackoffLimitedRetryHandler.java         |   74 +
 .../util/internal/ssh/ShellAbstractTool.java    |  442 ++++
 .../core/util/internal/ssh/ShellTool.java       |  113 +
 .../core/util/internal/ssh/SshAbstractTool.java |  172 ++
 .../core/util/internal/ssh/SshException.java    |   32 +
 .../core/util/internal/ssh/SshTool.java         |  174 ++
 .../core/util/internal/ssh/cli/SshCliTool.java  |  317 +++
 .../util/internal/ssh/process/ProcessTool.java  |  215 ++
 .../internal/ssh/sshj/SshjClientConnection.java |  282 +++
 .../core/util/internal/ssh/sshj/SshjTool.java   | 1091 ++++++++++
 .../core/util/javalang/ReflectionScanner.java   |  135 ++
 .../core/util/javalang/UrlClassLoader.java      |   70 +
 .../brooklyn/core/util/mutex/MutexSupport.java  |  119 ++
 .../core/util/mutex/SemaphoreForTasks.java      |  112 +
 .../core/util/mutex/SemaphoreWithOwners.java    |  231 ++
 .../brooklyn/core/util/mutex/WithMutexes.java   |   45 +
 .../apache/brooklyn/core/util/osgi/Osgis.java   |  720 +++++++
 .../util/task/AbstractExecutionContext.java     |   75 +
 .../core/util/task/BasicExecutionContext.java   |  221 ++
 .../core/util/task/BasicExecutionManager.java   |  755 +++++++
 .../brooklyn/core/util/task/BasicTask.java      |  892 ++++++++
 .../brooklyn/core/util/task/CanSetName.java     |   25 +
 .../brooklyn/core/util/task/CompoundTask.java   |  131 ++
 .../core/util/task/DeferredSupplier.java        |   38 +
 .../core/util/task/DynamicSequentialTask.java   |  480 +++++
 .../brooklyn/core/util/task/DynamicTasks.java   |  337 +++
 .../core/util/task/ExecutionListener.java       |   31 +
 .../brooklyn/core/util/task/ExecutionUtils.java |   49 +
 .../brooklyn/core/util/task/ForwardingTask.java |  325 +++
 .../util/task/ListenableForwardingFuture.java   |   50 +
 .../brooklyn/core/util/task/ParallelTask.java   |   85 +
 .../brooklyn/core/util/task/ScheduledTask.java  |  185 ++
 .../brooklyn/core/util/task/SequentialTask.java |   58 +
 .../core/util/task/SingleThreadedScheduler.java |  216 ++
 .../brooklyn/core/util/task/TaskBuilder.java    |  184 ++
 .../brooklyn/core/util/task/TaskInternal.java   |  125 ++
 .../brooklyn/core/util/task/TaskScheduler.java  |   41 +
 .../brooklyn/core/util/task/TaskTags.java       |   71 +
 .../apache/brooklyn/core/util/task/Tasks.java   |  488 +++++
 .../brooklyn/core/util/task/ValueResolver.java  |  426 ++++
 .../core/util/task/ssh/SshFetchTaskFactory.java |   88 +
 .../core/util/task/ssh/SshFetchTaskWrapper.java |  135 ++
 .../core/util/task/ssh/SshPutTaskFactory.java   |  123 ++
 .../core/util/task/ssh/SshPutTaskStub.java      |   69 +
 .../core/util/task/ssh/SshPutTaskWrapper.java   |  189 ++
 .../brooklyn/core/util/task/ssh/SshTasks.java   |  236 +++
 .../internal/AbstractSshExecTaskFactory.java    |   58 +
 .../ssh/internal/PlainSshExecTaskFactory.java   |   71 +
 .../util/task/system/ProcessTaskFactory.java    |   66 +
 .../core/util/task/system/ProcessTaskStub.java  |  102 +
 .../util/task/system/ProcessTaskWrapper.java    |  187 ++
 .../core/util/task/system/SystemTasks.java      |   29 +
 .../internal/AbstractProcessTaskFactory.java    |  216 ++
 .../system/internal/ExecWithLoggingHelpers.java |  202 ++
 .../internal/SystemProcessTaskFactory.java      |  131 ++
 .../core/util/text/DataUriSchemeParser.java     |  267 +++
 .../core/util/text/TemplateProcessor.java       |  398 ++++
 ...ompilerIndependentOuterClassFieldMapper.java |  166 ++
 .../xstream/EnumCaseForgivingConverter.java     |   60 +
 .../EnumCaseForgivingSingleValueConverter.java  |   35 +
 .../util/xstream/ImmutableListConverter.java    |   54 +
 .../util/xstream/ImmutableMapConverter.java     |   56 +
 .../util/xstream/ImmutableSetConverter.java     |   54 +
 .../util/xstream/Inet4AddressConverter.java     |   65 +
 .../core/util/xstream/MapConverter.java         |  104 +
 .../core/util/xstream/MutableSetConverter.java  |   44 +
 .../util/xstream/StringKeyMapConverter.java     |  134 ++
 .../core/util/xstream/XmlSerializer.java        |   97 +
 .../brooklyn/core/util/xstream/XmlUtil.java     |   59 +
 .../location/access/BrooklynAccessUtils.java    |    8 +-
 .../PortForwardManagerLocationResolver.java     |    3 +-
 .../location/basic/AbstractLocation.java        |    8 +-
 .../basic/AbstractLocationResolver.java         |    2 +-
 .../AggregatingMachineProvisioningLocation.java |    2 +-
 .../location/basic/BasicLocationRegistry.java   |    2 +-
 .../location/basic/BasicMachineDetails.java     |   10 +-
 .../location/basic/ByonLocationResolver.java    |    4 +-
 .../FixedListMachineProvisioningLocation.java   |    4 +-
 .../location/basic/HostLocationResolver.java    |    2 +-
 .../basic/LocalhostLocationResolver.java        |    3 +-
 .../LocalhostMachineProvisioningLocation.java   |   10 +-
 ...calhostPropertiesFromBrooklynProperties.java |    3 +-
 .../location/basic/LocationConfigUtils.java     |    8 +-
 .../location/basic/LocationDynamicType.java     |    2 +-
 .../location/basic/LocationInternal.java        |    4 +-
 ...ocationPropertiesFromBrooklynProperties.java |    4 +-
 .../location/basic/LocationTypeSnapshot.java    |    2 +-
 .../brooklyn/location/basic/MultiLocation.java  |    2 +-
 .../location/basic/NamedLocationResolver.java   |    2 +-
 .../brooklyn/location/basic/PortRanges.java     |    3 +-
 .../basic/SingleMachineLocationResolver.java    |    2 +-
 .../SingleMachineProvisioningLocation.java      |    3 +-
 .../location/basic/SshMachineLocation.java      |   32 +-
 ...bstractCloudMachineProvisioningLocation.java |    4 +-
 .../location/cloud/CloudLocationConfig.java     |    3 +-
 .../cloud/names/AbstractCloudMachineNamer.java  |    4 +-
 .../cloud/names/BasicCloudMachineNamer.java     |    4 +-
 .../location/cloud/names/CloudMachineNamer.java |    3 +-
 .../cloud/names/CustomMachineNamer.java         |    6 +-
 .../location/dynamic/DynamicLocation.java       |    2 +-
 .../location/dynamic/LocationOwner.java         |    2 +-
 .../brooklyn/location/geo/HostGeoInfo.java      |    2 +-
 .../location/geo/LocalhostExternalIpLoader.java |    4 +-
 ...pi.basic.internal.ApiObjectsFactoryInterface |    2 +-
 .../brooklyn/camp/lite/CampYamlLiteTest.java    |    4 +-
 .../camp/lite/TestAppAssemblyInstantiator.java  |    2 +-
 .../enricher/basic/BasicEnricherTest.java       |    2 +-
 .../java/brooklyn/entity/EffectorSayHiTest.java |    2 +-
 .../entity/EntityPreManagementTest.java         |    2 +-
 .../java/brooklyn/entity/SetFromFlagTest.java   |    2 +-
 .../brooklyn/entity/basic/ConfigMapTest.java    |    4 +-
 .../basic/DependentConfigurationTest.java       |    2 +-
 .../brooklyn/entity/basic/EntityConfigTest.java |    2 +-
 .../brooklyn/entity/basic/EntitySpecTest.java   |    4 +-
 ...apListAndOtherStructuredConfigKeyTest.groovy |    2 +-
 .../entity/basic/PolicyRegistrationTest.java    |    2 +-
 .../brooklyn/entity/basic/SanitizerTest.java    |    3 +-
 .../entity/effector/EffectorBasicTest.java      |    2 +-
 .../effector/EffectorConcatenateTest.java       |    4 +-
 .../entity/effector/EffectorTaskTest.java       |   10 +-
 .../entity/group/GroupPickUpEntitiesTest.java   |    2 +-
 .../java/brooklyn/entity/rebind/Dumpers.java    |    2 +-
 .../entity/rebind/RebindCatalogEntityTest.java  |    3 +-
 .../entity/rebind/RebindCatalogItemTest.java    |    3 +-
 .../entity/rebind/RebindEnricherTest.java       |    2 +-
 .../rebind/RebindEntityDynamicTypeInfoTest.java |    2 +-
 .../entity/rebind/RebindEntityTest.java         |    2 +-
 .../entity/rebind/RebindFailuresTest.java       |    4 +-
 .../brooklyn/entity/rebind/RebindFeedTest.java  |    4 +-
 .../entity/rebind/RebindFeedWithHaTest.java     |    4 +-
 .../entity/rebind/RebindLocationTest.java       |    2 +-
 .../entity/rebind/RebindManagerTest.java        |    5 +-
 .../entity/rebind/RebindPolicyTest.java         |    4 +-
 .../entity/rebind/RebindTestFixture.java        |    2 +-
 .../transformer/impl/XsltTransformerTest.java   |    4 +-
 .../brooklyn/entity/trait/FailingEntity.java    |    2 +-
 .../entity/trait/FailingEntityImpl.java         |    2 +-
 .../entity/trait/StartableMethodsTest.java      |    3 +-
 .../java/brooklyn/event/feed/PollerTest.java    |    2 +-
 .../brooklyn/event/feed/http/HttpFeedTest.java  |    4 +-
 .../event/feed/http/HttpValueFunctionsTest.java |    3 +-
 .../brooklyn/policy/basic/BasicPolicyTest.java  |   89 -
 .../brooklyn/policy/basic/EnricherTypeTest.java |   59 -
 .../brooklyn/policy/basic/PolicyConfigTest.java |  202 --
 .../policy/basic/PolicySubscriptionTest.java    |  125 --
 .../brooklyn/policy/basic/PolicyTypeTest.java   |   58 -
 .../EntityCleanupLongevityTestFixture.java      |    4 +-
 .../FilePersistencePerformanceTest.java         |    2 +-
 .../qa/performance/TaskPerformanceTest.java     |    4 +-
 .../test/java/brooklyn/test/HttpService.java    |    4 +-
 .../java/brooklyn/test/policy/TestEnricher.java |    2 +-
 .../java/brooklyn/test/policy/TestPolicy.java   |    4 +-
 .../util/BrooklynMavenArtifactsTest.java        |   96 -
 .../brooklyn/util/ResourceUtilsHttpTest.java    |  196 --
 .../java/brooklyn/util/ResourceUtilsTest.java   |  189 --
 .../brooklyn/util/config/ConfigBagTest.java     |  191 --
 .../util/crypto/SecureKeysAndSignerTest.java    |  166 --
 .../brooklyn/util/file/ArchiveBuilderTest.java  |  193 --
 .../brooklyn/util/file/ArchiveUtilsTest.java    |  135 --
 .../util/flags/MethodCoercionsTest.java         |  146 --
 .../brooklyn/util/http/BetterMockWebServer.java |  138 --
 .../util/http/HttpToolIntegrationTest.java      |   98 -
 .../brooklyn/util/internal/FlagUtilsTest.java   |  314 ---
 .../brooklyn/util/internal/RepeaterTest.groovy  |  255 ---
 .../util/internal/TypeCoercionsTest.java        |  360 ----
 .../util/internal/ssh/RecordingSshTool.java     |   95 -
 .../internal/ssh/ShellToolAbstractTest.java     |  439 ----
 .../ssh/SshToolAbstractIntegrationTest.java     |  301 ---
 .../ssh/SshToolAbstractPerformanceTest.java     |  137 --
 .../ssh/cli/SshCliToolIntegrationTest.java      |  119 --
 .../ssh/cli/SshCliToolPerformanceTest.java      |   44 -
 .../ssh/process/ProcessToolIntegrationTest.java |   69 -
 .../ssh/process/ProcessToolStaticsTest.java     |   79 -
 .../sshj/SshjToolAsyncStubIntegrationTest.java  |  177 --
 .../ssh/sshj/SshjToolIntegrationTest.java       |  313 ---
 .../ssh/sshj/SshjToolPerformanceTest.java       |   44 -
 .../brooklyn/util/mutex/WithMutexesTest.java    |  126 --
 .../test/java/brooklyn/util/osgi/OsgisTest.java |   41 -
 .../util/ssh/BashCommandsIntegrationTest.java   |  501 -----
 .../task/BasicTaskExecutionPerformanceTest.java |  206 --
 .../util/task/BasicTaskExecutionTest.java       |  460 ----
 .../util/task/BasicTasksFutureTest.java         |  224 --
 .../util/task/CompoundTaskExecutionTest.java    |  252 ---
 .../util/task/DynamicSequentialTaskTest.java    |  365 ----
 .../util/task/NonBasicTaskExecutionTest.java    |  126 --
 .../util/task/ScheduledExecutionTest.java       |  287 ---
 .../util/task/SingleThreadedSchedulerTest.java  |  192 --
 .../util/task/TaskFinalizationTest.java         |   62 -
 .../test/java/brooklyn/util/task/TasksTest.java |  181 --
 .../brooklyn/util/task/ValueResolverTest.java   |  132 --
 .../brooklyn/util/task/ssh/SshTasksTest.java    |  207 --
 .../util/task/system/SystemTasksTest.java       |  134 --
 .../util/text/DataUriSchemeParserTest.java      |   52 -
 .../util/text/TemplateProcessorTest.java        |  179 --
 .../util/xstream/CompilerCompatibilityTest.java |  154 --
 .../util/xstream/ConverterTestFixture.java      |   40 -
 .../xstream/EnumCaseForgivingConverterTest.java |   52 -
 .../xstream/ImmutableListConverterTest.java     |   59 -
 .../util/xstream/InetAddressConverterTest.java  |   41 -
 .../util/xstream/StringKeyMapConverterTest.java |   77 -
 .../java/brooklyn/util/xstream/XmlUtilTest.java |   33 -
 .../core/catalog/internal/CatalogDtoTest.java   |    2 +-
 .../core/catalog/internal/CatalogLoadTest.java  |    3 +-
 .../core/catalog/internal/CatalogScanTest.java  |    2 +-
 .../AcmeEntitlementManagerTestFixture.java      |    2 +-
 .../entitlement/EntityEntitlementTest.java      |    2 +-
 .../internal/EntityExecutionManagerTest.java    |    8 +-
 .../management/osgi/OsgiStandaloneTest.java     |    6 +-
 .../osgi/OsgiVersionMoreEntityTest.java         |    2 +-
 .../core/policy/basic/BasicPolicyTest.java      |   90 +
 .../core/policy/basic/EnricherTypeTest.java     |   59 +
 .../core/policy/basic/PolicyConfigTest.java     |  202 ++
 .../policy/basic/PolicySubscriptionTest.java    |  128 ++
 .../core/policy/basic/PolicyTypeTest.java       |   59 +
 .../core/util/BrooklynMavenArtifactsTest.java   |   98 +
 .../core/util/ResourceUtilsHttpTest.java        |  197 ++
 .../brooklyn/core/util/ResourceUtilsTest.java   |  190 ++
 .../core/util/config/ConfigBagTest.java         |  193 ++
 .../util/crypto/SecureKeysAndSignerTest.java    |  169 ++
 .../core/util/file/ArchiveBuilderTest.java      |  194 ++
 .../core/util/file/ArchiveUtilsTest.java        |  139 ++
 .../core/util/flags/MethodCoercionsTest.java    |  149 ++
 .../core/util/http/BetterMockWebServer.java     |  138 ++
 .../core/util/http/HttpToolIntegrationTest.java |  100 +
 .../core/util/internal/FlagUtilsTest.java       |  314 +++
 .../core/util/internal/RepeaterTest.groovy      |  257 +++
 .../core/util/internal/TypeCoercionsTest.java   |  360 ++++
 .../util/internal/ssh/RecordingSshTool.java     |   97 +
 .../internal/ssh/ShellToolAbstractTest.java     |  441 ++++
 .../ssh/SshToolAbstractIntegrationTest.java     |  304 +++
 .../ssh/SshToolAbstractPerformanceTest.java     |  138 ++
 .../ssh/cli/SshCliToolIntegrationTest.java      |  119 ++
 .../ssh/cli/SshCliToolPerformanceTest.java      |   44 +
 .../ssh/process/ProcessToolIntegrationTest.java |   69 +
 .../ssh/process/ProcessToolStaticsTest.java     |   80 +
 .../sshj/SshjToolAsyncStubIntegrationTest.java  |  178 ++
 .../ssh/sshj/SshjToolIntegrationTest.java       |  314 +++
 .../ssh/sshj/SshjToolPerformanceTest.java       |   44 +
 .../core/util/mutex/WithMutexesTest.java        |  129 ++
 .../brooklyn/core/util/osgi/OsgisTest.java      |   41 +
 .../util/ssh/BashCommandsIntegrationTest.java   |  504 +++++
 .../task/BasicTaskExecutionPerformanceTest.java |  209 ++
 .../core/util/task/BasicTaskExecutionTest.java  |  462 ++++
 .../core/util/task/BasicTasksFutureTest.java    |  227 ++
 .../util/task/CompoundTaskExecutionTest.java    |  258 +++
 .../util/task/DynamicSequentialTaskTest.java    |  371 ++++
 .../util/task/NonBasicTaskExecutionTest.java    |  130 ++
 .../core/util/task/ScheduledExecutionTest.java  |  291 +++
 .../util/task/SingleThreadedSchedulerTest.java  |  195 ++
 .../core/util/task/TaskFinalizationTest.java    |   63 +
 .../brooklyn/core/util/task/TasksTest.java      |  184 ++
 .../core/util/task/ValueResolverTest.java       |  134 ++
 .../core/util/task/ssh/SshTasksTest.java        |  213 ++
 .../core/util/task/system/SystemTasksTest.java  |  137 ++
 .../core/util/text/DataUriSchemeParserTest.java |   53 +
 .../core/util/text/TemplateProcessorTest.java   |  180 ++
 .../util/xstream/CompilerCompatibilityTest.java |  158 ++
 .../core/util/xstream/ConverterTestFixture.java |   40 +
 .../xstream/EnumCaseForgivingConverterTest.java |   53 +
 .../xstream/ImmutableListConverterTest.java     |   60 +
 .../util/xstream/InetAddressConverterTest.java  |   42 +
 .../util/xstream/StringKeyMapConverterTest.java |   78 +
 .../brooklyn/core/util/xstream/XmlUtilTest.java |   34 +
 .../location/basic/AbstractLocationTest.java    |    2 +-
 .../basic/LegacyAbstractLocationTest.java       |    2 +-
 .../location/basic/LocationConfigTest.java      |    2 +-
 .../location/basic/LocationConfigUtilsTest.java |    3 +-
 .../brooklyn/location/basic/PortRangesTest.java |    3 +-
 .../SshMachineLocationIntegrationTest.java      |    9 +-
 .../SshMachineLocationPerformanceTest.java      |    2 +-
 .../SshMachineLocationReuseIntegrationTest.java |    4 +-
 .../location/basic/SshMachineLocationTest.java  |   14 +-
 .../location/cloud/CloudMachineNamerTest.java   |    4 +-
 .../location/cloud/CustomMachineNamerTest.java  |    3 +-
 .../brooklyn/test/entity/BlockingEntity.java    |    2 +-
 .../apache/brooklyn/test/entity/TestEntity.java |    2 +-
 .../rebind/compiler_compatibility_eclipse.xml   |   20 +-
 .../rebind/compiler_compatibility_oracle.xml    |   18 +-
 docs/guide/dev/code/licensing.md                |    6 +
 .../brooklyn/demo/GlobalWebFabricExample.java   |    6 +-
 .../brooklyn/demo/CumulusRDFApplication.java    |    8 +-
 .../demo/WebClusterDatabaseExampleApp.java      |    6 +-
 .../demo/WebClusterDatabaseExampleGroovy.groovy |    6 +-
 .../policy/os/AdvertiseWinrmLoginPolicy.java    |    3 +-
 .../brooklyn/policy/os/CreateUserPolicy.java    |    6 +-
 .../location/jclouds/BrooklynMachinePool.java   |    2 +-
 .../jclouds/ComputeServiceRegistry.java         |    3 +-
 .../jclouds/ComputeServiceRegistryImpl.java     |    2 +-
 .../jclouds/JcloudsByonLocationResolver.java    |    2 +-
 .../location/jclouds/JcloudsLocation.java       |   18 +-
 .../location/jclouds/JcloudsLocationConfig.java |    3 +-
 .../jclouds/JcloudsLocationCustomizer.java      |    3 +-
 .../location/jclouds/JcloudsMachineNamer.java   |    2 +-
 ...JcloudsPropertiesFromBrooklynProperties.java |    4 +-
 .../jclouds/JcloudsSshMachineLocation.java      |    2 +-
 .../brooklyn/location/jclouds/JcloudsUtil.java  |    2 +-
 .../jclouds/JcloudsWinRmMachineLocation.java    |    2 +-
 .../jclouds/SudoTtyFixingCustomizer.java        |    7 +-
 .../JcloudsLocationSecurityGroupCustomizer.java |    4 +-
 .../persister/jclouds/BlobStoreExpiryTest.java  |    6 +-
 .../policy/os/CreateUserPolicyLiveTest.java     |    2 +-
 .../policy/os/CreateUserPolicyTest.java         |    3 +-
 .../jclouds/AbstractJcloudsStubbedLiveTest.java |    3 +-
 .../jclouds/BailOutJcloudsLocation.java         |    2 +-
 ...ationTemplateOptionsCustomisersLiveTest.java |    4 +-
 .../location/jclouds/JcloudsLocationTest.java   |    2 +-
 .../jclouds/JcloudsMachineNamerTest.java        |    2 +-
 .../jclouds/RebindJcloudsLocationLiveTest.java  |    2 +-
 .../jclouds/RebindJcloudsLocationTest.java      |    3 +-
 .../java/brooklyn/enricher/DeltaEnricher.java   |    2 +-
 .../brooklyn/enricher/HttpLatencyDetector.java  |    2 +-
 .../brooklyn/enricher/RollingMeanEnricher.java  |    2 +-
 .../enricher/RollingTimeWindowMeanEnricher.java |    2 +-
 .../enricher/TimeFractionDeltaEnricher.java     |    2 +-
 .../enricher/TimeWeightedDeltaEnricher.java     |    2 +-
 .../entity/brooklyn/BrooklynMetrics.java        |    2 +-
 .../entity/brooklyn/BrooklynMetricsImpl.java    |    2 +-
 .../policy/autoscaling/AutoScalerPolicy.java    |    8 +-
 .../policy/followthesun/FollowTheSunPolicy.java |    4 +-
 .../policy/ha/AbstractFailureDetector.java      |    8 +-
 .../policy/ha/ConditionalSuspendPolicy.java     |    4 +-
 .../policy/ha/ConnectionFailureDetector.java    |    2 +-
 .../policy/ha/ServiceFailureDetector.java       |    8 +-
 .../brooklyn/policy/ha/ServiceReplacer.java     |    6 +-
 .../brooklyn/policy/ha/ServiceRestarter.java    |    6 +-
 .../policy/ha/SshMachineFailureDetector.java    |    4 +-
 .../loadbalancing/ItemsInContainersGroup.java   |    2 +-
 .../loadbalancing/LoadBalancingPolicy.java      |    4 +-
 .../brooklyn/policy/loadbalancing/Movable.java  |    2 +-
 .../enricher/HttpLatencyDetectorTest.java       |    4 +-
 .../brooklyn/enricher/RebindEnricherTest.java   |    2 +-
 .../brooklyn/policy/ha/ServiceReplacerTest.java |    2 +-
 .../policy/ha/ServiceRestarterTest.java         |    2 +-
 .../loadbalancing/MockContainerEntity.java      |    2 +-
 .../entity/database/derby/DerbyDatabase.java    |    4 +-
 .../database/derby/DerbyDatabaseSshDriver.java  |    4 +-
 .../entity/database/derby/DerbySchema.java      |    8 +-
 .../postgresql/PostgreSqlNodeSaltImpl.java      |   12 +-
 .../apache/brooklyn/entity/salt/SaltConfig.java |   11 +-
 .../brooklyn/entity/salt/SaltConfigs.java       |    4 +-
 .../entity/salt/SaltLifecycleEffectorTasks.java |    8 +-
 .../brooklyn/entity/salt/SaltStackMaster.java   |    9 +-
 .../entity/salt/SaltStackMasterSshDriver.java   |    5 +-
 .../apache/brooklyn/entity/salt/SaltTasks.java  |   13 +-
 .../postgresql/PostgreSqlSaltLiveTest.java      |   11 +-
 .../brooklyn/entity/salt/SaltConfigsTest.java   |    2 +-
 .../entity/salt/SaltLiveTestSupport.java        |    8 +-
 .../entity/monitoring/zabbix/ZabbixFeed.java    |    4 +-
 .../monitoring/zabbix/ZabbixMonitored.java      |    2 +-
 .../monitoring/zabbix/ZabbixPollConfig.java     |    2 +-
 .../entity/monitoring/zabbix/ZabbixServer.java  |    2 +-
 .../nosql/hazelcast/HazelcastCluster.java       |    9 +-
 .../nosql/hazelcast/HazelcastClusterImpl.java   |    8 +-
 .../entity/nosql/hazelcast/HazelcastNode.java   |    9 +-
 .../nosql/hazelcast/HazelcastNodeImpl.java      |    2 +-
 .../nosql/hazelcast/HazelcastNodeSshDriver.java |    6 +-
 .../nosql/infinispan/Infinispan5Server.java     |    4 +-
 .../nosql/infinispan/Infinispan5SshDriver.java  |    4 +-
 .../hazelcast/HazelcastClusterEc2LiveTest.java  |    4 +-
 .../HazelcastClusterSoftlayerLiveTest.java      |    4 +-
 .../Infinispan5ServerIntegrationTest.groovy     |    6 +-
 .../basic/AbstractSoftwareProcessDriver.java    |    8 +-
 .../basic/AbstractSoftwareProcessSshDriver.java |   12 +-
 .../SameServerDriverLifecycleEffectorTasks.java |    4 +-
 .../brooklyn/entity/basic/SameServerEntity.java |    2 +-
 .../entity/basic/SameServerEntityImpl.java      |    6 +-
 .../brooklyn/entity/basic/SoftwareProcess.java  |    2 +-
 ...wareProcessDriverLifecycleEffectorTasks.java |    4 +-
 .../entity/basic/SoftwareProcessImpl.java       |    8 +-
 .../basic/VanillaSoftwareProcessSshDriver.java  |    4 +-
 .../basic/lifecycle/NaiveScriptRunner.java      |    2 +-
 .../entity/basic/lifecycle/ScriptHelper.java    |   12 +-
 .../brooklynnode/BrooklynEntityMirrorImpl.java  |    8 +-
 .../entity/brooklynnode/BrooklynNode.java       |    2 +-
 .../entity/brooklynnode/BrooklynNodeImpl.java   |   12 +-
 .../brooklynnode/BrooklynNodeSshDriver.java     |   10 +-
 .../entity/brooklynnode/EntityHttpClient.java   |    4 +-
 .../brooklynnode/EntityHttpClientImpl.java      |    6 +-
 .../brooklynnode/RemoteEffectorBuilder.java     |    2 +-
 .../BrooklynClusterUpgradeEffectorBody.java     |    6 +-
 .../BrooklynNodeUpgradeEffectorBody.java        |    6 +-
 .../effector/SelectMasterEffectorBody.java      |    4 +-
 .../SetHighAvailabilityModeEffectorBody.java    |    4 +-
 ...SetHighAvailabilityPriorityEffectorBody.java |    4 +-
 .../brooklyn/entity/chef/ChefAttributeFeed.java |    4 +-
 .../java/brooklyn/entity/chef/ChefConfig.java   |    3 +-
 .../entity/chef/ChefLifecycleEffectorTasks.java |   10 +-
 .../brooklyn/entity/chef/ChefServerTasks.java   |    2 +-
 .../brooklyn/entity/chef/ChefSoloDriver.java    |    3 +-
 .../java/brooklyn/entity/chef/ChefTasks.java    |   10 +-
 .../entity/chef/KnifeConvergeTaskFactory.java   |    4 +-
 .../brooklyn/entity/chef/KnifeTaskFactory.java  |   10 +-
 .../java/JavaSoftwareProcessSshDriver.java      |   16 +-
 .../java/brooklyn/entity/java/JmxSupport.java   |    8 +-
 .../brooklyn/entity/java/JmxmpSslSupport.java   |    7 +-
 .../java/brooklyn/entity/java/UsesJava.java     |    3 +-
 .../brooklyn/entity/java/UsesJavaMXBeans.java   |    2 +-
 .../main/java/brooklyn/entity/java/UsesJmx.java |    3 +-
 .../brooklyn/entity/java/VanillaJavaApp.java    |    2 +-
 .../entity/java/VanillaJavaAppImpl.java         |    2 +-
 .../entity/java/VanillaJavaAppSshDriver.java    |   10 +-
 .../entity/machine/MachineEntityImpl.java       |    6 +-
 .../brooklyn/entity/pool/ServerPoolImpl.java    |    2 +-
 .../entity/pool/ServerPoolLocation.java         |    3 +-
 .../entity/service/EntityLaunchListener.java    |    2 +-
 .../entity/service/InitdServiceInstaller.java   |   12 +-
 .../entity/service/SystemServiceEnricher.java   |   14 +-
 .../entity/software/MachineInitTasks.java       |    8 +-
 .../software/MachineLifecycleEffectorTasks.java |    8 +-
 .../software/ProvidesProvisioningFlags.java     |    3 +-
 .../entity/software/SshEffectorTasks.java       |   24 +-
 .../brooklyn/entity/software/StaticSensor.java  |    6 +-
 .../entity/software/http/HttpRequestSensor.java |    2 +-
 .../software/java/JmxAttributeSensor.java       |    6 +-
 .../entity/software/ssh/SshCommandEffector.java |    2 +-
 .../entity/software/ssh/SshCommandSensor.java   |    4 +-
 .../winrm/WindowsPerformanceCounterSensors.java |    2 +-
 .../java/brooklyn/event/feed/jmx/JmxHelper.java |    2 +-
 .../basic/SoftwareProcessEntityLatchTest.java   |    2 +-
 .../basic/SoftwareProcessEntityRebindTest.java  |    3 +-
 .../entity/basic/SoftwareProcessEntityTest.java |    6 +-
 ...SoftwareProcessSshDriverIntegrationTest.java |    2 +-
 ...ftwareProcessAndChildrenIntegrationTest.java |    2 +-
 .../entity/basic/lifecycle/MyEntityImpl.java    |    6 +-
 .../basic/lifecycle/NaiveScriptRunnerTest.java  |    6 +-
 .../basic/lifecycle/StartStopSshDriverTest.java |    8 +-
 .../BrooklynNodeIntegrationTest.java            |    8 +-
 .../brooklynnode/CallbackEntityHttpClient.java  |    4 +-
 .../entity/chef/ChefLiveTestSupport.java        |    2 +-
 .../chef/ChefServerTasksIntegrationTest.java    |    2 +-
 .../ChefSoloDriverMySqlEntityLiveTest.java      |    2 +-
 .../java/brooklyn/entity/java/JavaOptsTest.java |    6 +-
 .../brooklyn/entity/java/JmxSupportTest.java    |    4 +-
 .../brooklyn/entity/java/SslKeyConfigTest.java  |    5 +-
 .../entity/java/VanillaJavaAppRebindTest.java   |    2 +-
 .../entity/java/VanillaJavaAppTest.java         |    8 +-
 .../MachineLifecycleEffectorTasksTest.java      |    4 +-
 .../entity/software/SoftwareEffectorTest.java   |    5 +-
 .../entity/software/SshEffectorTasksTest.java   |    6 +-
 .../entity/software/StaticSensorTest.java       |    2 +-
 .../software/http/HttpRequestSensorTest.java    |    2 +-
 .../mysql/AbstractToyMySqlEntityTest.java       |    2 +-
 .../mysql/DynamicToyMySqlEntityBuilder.java     |    8 +-
 .../software/ssh/SshCommandIntegrationTest.java |    2 +-
 software/database/pom.xml                       |    4 -
 .../entity/database/DatastoreMixins.java        |    4 +-
 .../entity/database/crate/CrateNode.java        |    3 +-
 .../entity/database/mariadb/MariaDbDriver.java  |    3 +-
 .../entity/database/mariadb/MariaDbNode.java    |    3 +-
 .../database/mariadb/MariaDbNodeImpl.java       |    4 +-
 .../database/mariadb/MariaDbSshDriver.java      |    4 +-
 .../entity/database/mysql/MySqlClusterImpl.java |    4 +-
 .../entity/database/mysql/MySqlDriver.java      |    3 +-
 .../entity/database/mysql/MySqlNode.java        |    3 +-
 .../entity/database/mysql/MySqlNodeImpl.java    |    4 +-
 .../entity/database/mysql/MySqlSshDriver.java   |    4 +-
 .../database/postgresql/PostgreSqlDriver.java   |    3 +-
 .../database/postgresql/PostgreSqlNode.java     |    4 +-
 .../PostgreSqlNodeChefImplFromScratch.java      |    8 +-
 .../database/postgresql/PostgreSqlNodeImpl.java |    2 +-
 .../postgresql/PostgreSqlSshDriver.java         |    8 +-
 .../entity/database/rubyrep/RubyRepNode.java    |    2 +-
 .../database/postgresql/PostgreSqlChefTest.java |    2 +-
 .../messaging/activemq/ActiveMQBroker.java      |    2 +-
 .../entity/messaging/amqp/AmqpExchange.java     |    2 +-
 .../brooklyn/entity/messaging/kafka/Kafka.java  |    3 +-
 .../entity/messaging/kafka/KafkaBroker.java     |    4 +-
 .../entity/messaging/kafka/KafkaCluster.java    |    2 +-
 .../entity/messaging/kafka/KafkaZooKeeper.java  |    2 +-
 .../entity/messaging/qpid/QpidBroker.java       |    2 +-
 .../messaging/qpid/QpidDestinationImpl.java     |    2 +-
 .../entity/messaging/rabbit/RabbitBroker.java   |    2 +-
 .../brooklyn/entity/messaging/storm/Storm.java  |    2 +-
 .../entity/messaging/storm/StormDeployment.java |    2 +-
 .../messaging/storm/StormDeploymentImpl.java    |    2 +-
 .../entity/zookeeper/ZooKeeperEnsemble.java     |    2 +-
 .../entity/zookeeper/ZooKeeperNode.java         |    2 +-
 .../storm/StormAbstractCloudLiveTest.java       |    4 +-
 .../entity/monitoring/monit/MonitNode.java      |    2 +-
 .../entity/network/bind/BindDnsServer.java      |    4 +-
 .../nosql/cassandra/CassandraDatacenter.java    |    2 +-
 .../cassandra/CassandraDatacenterImpl.java      |    4 +-
 .../entity/nosql/cassandra/CassandraNode.java   |    4 +-
 .../nosql/cassandra/CassandraNodeDriver.java    |    3 +-
 .../nosql/cassandra/CassandraNodeImpl.java      |    4 +-
 .../nosql/cassandra/CassandraNodeSshDriver.java |    8 +-
 .../nosql/couchbase/CouchbaseCluster.java       |    2 +-
 .../nosql/couchbase/CouchbaseClusterImpl.java   |   10 +-
 .../entity/nosql/couchbase/CouchbaseNode.java   |    2 +-
 .../nosql/couchbase/CouchbaseNodeImpl.java      |    8 +-
 .../nosql/couchbase/CouchbaseNodeSshDriver.java |   12 +-
 .../nosql/couchbase/CouchbaseSyncGateway.java   |    2 +-
 .../entity/nosql/couchdb/CouchDBCluster.java    |    2 +-
 .../entity/nosql/couchdb/CouchDBNode.java       |    2 +-
 .../entity/nosql/couchdb/CouchDBNodeImpl.java   |    2 +-
 .../elasticsearch/ElasticSearchCluster.java     |    2 +-
 .../nosql/elasticsearch/ElasticSearchNode.java  |    3 +-
 .../elasticsearch/ElasticSearchNodeImpl.java    |    4 +-
 .../nosql/mongodb/AbstractMongoDBServer.java    |    2 +-
 .../entity/nosql/mongodb/MongoDBClient.java     |    3 +-
 .../entity/nosql/mongodb/MongoDBReplicaSet.java |    2 +-
 .../entity/nosql/mongodb/MongoDBServer.java     |    2 +-
 .../sharding/CoLocatedMongoDBRouter.java        |    2 +-
 .../sharding/MongoDBShardedDeployment.java      |    2 +-
 .../brooklyn/entity/nosql/redis/RedisSlave.java |    2 +-
 .../brooklyn/entity/nosql/redis/RedisStore.java |    2 +-
 .../brooklyn/entity/nosql/riak/RiakCluster.java |    2 +-
 .../entity/nosql/riak/RiakClusterImpl.java      |    2 +-
 .../brooklyn/entity/nosql/riak/RiakNode.java    |    2 +-
 .../entity/nosql/riak/RiakNodeImpl.java         |    2 +-
 .../entity/nosql/riak/RiakNodeSshDriver.java    |    4 +-
 .../brooklyn/entity/nosql/solr/SolrServer.java  |    4 +-
 .../entity/nosql/solr/SolrServerSshDriver.java  |    2 +-
 .../cassandra/CassandraDatacenterTest.java      |    4 +-
 .../ElasticSearchClusterIntegrationTest.java    |    4 +-
 .../ElasticSearchNodeIntegrationTest.java       |    5 +-
 .../entity/osgi/karaf/KarafContainer.java       |    2 +-
 .../entity/dns/AbstractGeoDnsServiceImpl.java   |    2 +-
 .../dns/geoscaling/GeoscalingDnsService.java    |    2 +-
 .../geoscaling/GeoscalingDnsServiceImpl.java    |    4 +-
 .../geoscaling/GeoscalingScriptGenerator.java   |    3 +-
 .../dns/geoscaling/GeoscalingWebClient.java     |    2 +-
 .../entity/proxy/AbstractController.java        |    2 +-
 .../entity/proxy/AbstractControllerImpl.java    |    2 +-
 .../brooklyn/entity/proxy/LoadBalancer.java     |    2 +-
 .../brooklyn/entity/proxy/ProxySslConfig.java   |    5 +-
 .../entity/proxy/nginx/NginxController.java     |    2 +-
 .../entity/proxy/nginx/NginxControllerImpl.java |    8 +-
 .../entity/proxy/nginx/NginxSshDriver.java      |    8 +-
 .../nginx/NginxTemplateConfigGenerator.java     |    4 +-
 .../brooklyn/entity/proxy/nginx/UrlMapping.java |    2 +-
 .../webapp/ControlledDynamicWebAppCluster.java  |    2 +-
 .../entity/webapp/DynamicWebAppClusterImpl.java |    8 +-
 .../entity/webapp/JavaWebAppService.java        |    2 +-
 .../entity/webapp/JavaWebAppSshDriver.java      |    8 +-
 .../entity/webapp/WebAppServiceConstants.java   |    2 +-
 .../entity/webapp/jboss/JBoss6Server.java       |    2 +-
 .../entity/webapp/jboss/JBoss7Server.java       |    2 +-
 .../entity/webapp/jetty/Jetty6Server.java       |    2 +-
 .../webapp/nodejs/NodeJsWebAppService.java      |    3 +-
 .../webapp/nodejs/NodeJsWebAppSshDriver.java    |    4 +-
 .../entity/webapp/tomcat/Tomcat8Server.java     |    2 +-
 .../entity/webapp/tomcat/TomcatServer.java      |    4 +-
 .../GeoscalingScriptGeneratorTest.java          |    3 +-
 .../dns/geoscaling/GeoscalingWebClientTest.java |    2 +-
 .../entity/proxy/AbstractControllerTest.java    |    2 +-
 .../entity/proxy/ProxySslConfigTest.java        |    2 +-
 .../nginx/NginxRebindWithHaIntegrationTest.java |    4 +-
 .../AbstractWebAppFixtureIntegrationTest.java   |    4 +-
 .../entity/webapp/HttpsSslConfigTest.java       |    2 +-
 .../webapp/WebAppConcurrentDeployTest.java      |    4 +-
 .../test/entity/TestJavaWebAppEntity.java       |    2 +-
 .../test/entity/TestJavaWebAppEntityImpl.java   |    2 +-
 .../app/SampleLocalhostIntegrationTest.java     |    2 +-
 .../camp/brooklyn/YamlLauncherAbstract.java     |    2 +-
 .../BrooklynAssemblyTemplateInstantiator.java   |    4 +-
 .../BrooklynComponentTemplateResolver.java      |   10 +-
 .../BrooklynEntityDecorationResolver.java       |    2 +-
 .../creation/BrooklynYamlTypeInstantiator.java  |    2 +-
 .../spi/dsl/BrooklynDslDeferredSupplier.java    |    2 +-
 .../camp/brooklyn/spi/dsl/DslUtils.java         |    2 +-
 .../spi/dsl/methods/BrooklynDslCommon.java      |   10 +-
 .../brooklyn/spi/dsl/methods/DslComponent.java  |    4 +-
 .../camp/brooklyn/AbstractYamlRebindTest.java   |    4 +-
 .../camp/brooklyn/AbstractYamlTest.java         |    4 +-
 .../camp/brooklyn/DslAndRebindYamlTest.java     |    2 +-
 .../camp/brooklyn/EntitiesYamlTest.java         |    2 +-
 ...aWebAppWithDslYamlRebindIntegrationTest.java |    2 +-
 .../brooklyn/JavaWebAppsIntegrationTest.java    |    2 +-
 .../camp/brooklyn/JavaWebAppsMatchingTest.java  |    4 +-
 .../camp/brooklyn/MapReferenceYamlTest.java     |    2 +-
 .../brooklyn/camp/brooklyn/ObjectsYamlTest.java |    6 +-
 .../brooklyn/ReloadBrooklynPropertiesTest.java  |    2 +-
 .../camp/brooklyn/TestReferencingPolicy.java    |    2 +-
 .../TestSensorAndEffectorInitializer.java       |    2 +-
 .../catalog/AbstractCatalogXmlTest.java         |    2 +-
 .../CatalogOsgiVersionMoreEntityTest.java       |    4 +-
 .../brooklyn/catalog/CatalogYamlEntityTest.java |    2 +-
 usage/cli/pom.xml                               |    4 +-
 .../org/apache/brooklyn/cli/ItemLister.java     |    6 +-
 .../main/java/org/apache/brooklyn/cli/Main.java |   15 +-
 .../apache/brooklyn/cli/lister/ClassFinder.java |    6 +-
 .../brooklyn/cli/lister/ItemDescriptors.java    |    4 +-
 usage/cli/src/main/license/DISCLAIMER           |    8 -
 usage/cli/src/main/license/LICENSE              |  208 --
 usage/cli/src/main/license/NOTICE               |    5 -
 usage/cli/src/main/license/README.md            |    7 +
 usage/cli/src/main/license/files/DISCLAIMER     |    8 +
 usage/cli/src/main/license/files/LICENSE        |  239 +++
 usage/cli/src/main/license/files/NOTICE         |    5 +
 .../cli/src/main/license/source-inclusions.yaml |   24 +
 .../statics/brooklyn-object-list.html           |    2 +-
 .../statics/style/js/catalog/bloodhound.js      |  727 -------
 .../statics/style/js/catalog/typeahead.js       |  727 +++++++
 .../java/org/apache/brooklyn/cli/CliTest.java   |    2 +-
 usage/cli/src/test/license/DISCLAIMER           |    8 -
 usage/cli/src/test/license/LICENSE              |  175 --
 usage/cli/src/test/license/NOTICE               |    5 -
 usage/cli/src/test/license/files/DISCLAIMER     |    8 +
 usage/cli/src/test/license/files/LICENSE        |  175 ++
 usage/cli/src/test/license/files/NOTICE         |    5 +
 usage/dist/licensing/.gitignore                 |    2 +
 usage/dist/licensing/MAIN_LICENSE_ASL2          |  176 ++
 usage/dist/licensing/README.md                  |   77 +
 usage/dist/licensing/extras-files               |    1 +
 usage/dist/licensing/licenses/binary/ASL2       |  177 ++
 .../dist/licensing/licenses/binary/BSD-2-Clause |   23 +
 .../dist/licensing/licenses/binary/BSD-3-Clause |   27 +
 usage/dist/licensing/licenses/binary/CDDL1      |  381 ++++
 usage/dist/licensing/licenses/binary/CDDL1.1    |  304 +++
 usage/dist/licensing/licenses/binary/EPL1       |  212 ++
 usage/dist/licensing/licenses/binary/MIT        |   20 +
 usage/dist/licensing/licenses/binary/WTFPL      |   15 +
 .../dist/licensing/licenses/binary/bouncycastle |   23 +
 usage/dist/licensing/licenses/binary/jtidy      |   53 +
 usage/dist/licensing/licenses/binary/jython     |   27 +
 .../licenses/binary/metastuff-bsd-style         |   43 +
 .../licenses/binary/xpp3_indiana_university     |   45 +
 usage/dist/licensing/licenses/cli/MIT           |   20 +
 .../dist/licensing/licenses/jsgui/BSD-3-Clause  |   27 +
 usage/dist/licensing/licenses/jsgui/MIT         |   20 +
 .../dist/licensing/licenses/source/BSD-3-Clause |   27 +
 usage/dist/licensing/licenses/source/MIT        |   20 +
 usage/dist/licensing/make-all-licenses.sh       |   61 +
 usage/dist/licensing/make-one-license.sh        |   77 +
 usage/dist/licensing/overrides.yaml             |  220 ++
 .../licensing/projects-with-custom-licenses     |    2 +
 usage/dist/pom.xml                              |    6 +
 .../dist/src/main/config/build-distribution.xml |    2 +-
 usage/dist/src/main/license/DISCLAIMER          |    8 -
 usage/dist/src/main/license/LICENSE             | 1691 ---------------
 usage/dist/src/main/license/NOTICE              |    5 -
 usage/dist/src/main/license/README.md           |    2 +
 usage/dist/src/main/license/files/DISCLAIMER    |    8 +
 usage/dist/src/main/license/files/LICENSE       | 1987 ++++++++++++++++++
 usage/dist/src/main/license/files/NOTICE        |    5 +
 usage/jsgui/pom.xml                             |    4 +-
 usage/jsgui/src/main/license/DISCLAIMER         |    8 -
 usage/jsgui/src/main/license/LICENSE            |  292 ---
 usage/jsgui/src/main/license/NOTICE             |    5 -
 usage/jsgui/src/main/license/README.md          |    7 +
 usage/jsgui/src/main/license/files/DISCLAIMER   |    8 +
 usage/jsgui/src/main/license/files/LICENSE      |  354 ++++
 usage/jsgui/src/main/license/files/NOTICE       |    5 +
 .../src/main/license/source-inclusions.yaml     |   39 +
 .../brooklyn/launcher/BrooklynWebServer.java    |   14 +-
 .../launcher/config/CustomResourceLocator.java  |    2 +-
 .../entity/basic/VanillaSoftwareYamlTest.java   |    2 +-
 .../brooklynnode/BrooklynNodeRestTest.java      |    6 +-
 .../database/mssql/MssqlBlueprintLiveTest.java  |    5 +-
 .../BrooklynLauncherRebindCatalogTest.java      |    2 +-
 .../launcher/BrooklynWebServerTest.java         |    4 +-
 .../blueprints/AbstractBlueprintTest.java       |    2 +-
 .../qa/load/SimulatedMySqlNodeImpl.java         |    6 +-
 .../brooklyn/qa/longevity/MonitorUtils.java     |    4 +-
 .../SoftlayerObtainPrivateLiveTest.java         |    4 +-
 .../resources/AbstractBrooklynRestResource.java |    2 +-
 .../rest/resources/ApplicationResource.java     |    2 +-
 .../rest/resources/CatalogResource.java         |    2 +-
 .../rest/resources/EntityConfigResource.java    |    4 +-
 .../brooklyn/rest/resources/EntityResource.java |    2 +-
 .../rest/resources/PolicyConfigResource.java    |    5 +-
 .../brooklyn/rest/resources/PolicyResource.java |    4 +-
 .../brooklyn/rest/resources/SensorResource.java |    2 +-
 .../brooklyn/rest/resources/ServerResource.java |    6 +-
 .../rest/transform/CatalogTransformer.java      |    2 +-
 .../rest/transform/EffectorTransformer.java     |    4 +-
 .../rest/transform/LocationTransformer.java     |    2 +-
 .../rest/transform/PolicyTransformer.java       |    2 +-
 .../rest/transform/TaskTransformer.java         |    2 +-
 .../rest/util/BrooklynRestResourceUtils.java    |    6 +-
 .../rest/util/DefaultExceptionMapper.java       |    2 +-
 .../BrooklynPropertiesSecurityFilterTest.java   |    6 +-
 .../brooklyn/rest/HaMasterCheckFilterTest.java  |    4 +-
 .../rest/resources/CatalogResetTest.java        |    2 +-
 .../SensorResourceIntegrationTest.java          |    4 +-
 .../ServerResourceIntegrationTest.java          |    7 +-
 .../rest/testing/mocks/CapitalizePolicy.java    |    3 +-
 .../testing/mocks/RestMockSimpleEntity.java     |    3 +-
 .../testing/mocks/RestMockSimplePolicy.java     |    4 +-
 .../util/BrooklynRestResourceUtilsTest.java     |    2 +-
 .../json/BrooklynJacksonSerializerTest.java     |    2 +-
 .../util/jmx/jmxmp/JmxmpAgentSslTest.java       |    5 +-
 .../brooklyn/util/jmx/jmxmp/JmxmpClient.java    |    3 +-
 .../brooklyn/osgi/tests/SimpleLocation.java     |    3 +-
 .../java/brooklyn/osgi/tests/SimplePolicy.java  |    5 +-
 .../osgi/tests/more/MoreEntityImpl.java         |    3 +-
 .../brooklyn/osgi/tests/more/MorePolicy.java    |    3 +-
 .../osgi/tests/more/MoreEntityImpl.java         |    2 +-
 .../osgi/tests/more/MoreEntityImpl.java         |    2 +-
 .../brooklyn/osgi/tests/more/MorePolicy.java    |    2 +-
 951 files changed, 36820 insertions(+), 34031 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/8cba4d3c/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
----------------------------------------------------------------------
diff --cc software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
index e8496f3,20f4b76..b90d19e
--- a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
+++ b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
@@@ -31,9 -31,9 +31,9 @@@ import brooklyn.entity.database.Databas
  import brooklyn.entity.database.DatastoreMixins;
  import brooklyn.entity.database.DatastoreMixins.DatastoreCommon;
  import brooklyn.entity.effector.Effectors;
 +import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
  import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
- import brooklyn.util.flags.SetFromFlag;
 -
+ import org.apache.brooklyn.location.basic.PortRanges;
  
  /**
   * PostgreSQL database node entity.

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/8cba4d3c/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
----------------------------------------------------------------------
diff --cc software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
index 0242af1,5ff0175..b7b5722
--- a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
+++ b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
@@@ -47,8 -47,11 +47,12 @@@ import brooklyn.entity.basic.SoftwarePr
  import brooklyn.entity.database.DatastoreMixins;
  import brooklyn.entity.software.SshEffectorTasks;
  
 +import org.apache.brooklyn.api.entity.basic.EntityLocal;
  import org.apache.brooklyn.api.location.OsDetails;
+ import org.apache.brooklyn.core.util.task.DynamicTasks;
+ import org.apache.brooklyn.core.util.task.ssh.SshTasks;
+ import org.apache.brooklyn.core.util.task.ssh.SshTasks.OnFailingTask;
+ import org.apache.brooklyn.core.util.task.system.ProcessTaskWrapper;
  import org.apache.brooklyn.location.basic.SshMachineLocation;
  
  import brooklyn.util.collections.MutableList;
@@@ -57,12 -60,7 +61,8 @@@ import brooklyn.util.exceptions.Excepti
  import brooklyn.util.net.Urls;
  import brooklyn.util.os.Os;
  import brooklyn.util.stream.Streams;
- import brooklyn.util.task.DynamicTasks;
- import brooklyn.util.task.ssh.SshTasks;
- import brooklyn.util.task.ssh.SshTasks.OnFailingTask;
- import brooklyn.util.task.system.ProcessTaskWrapper;
  import brooklyn.util.text.Identifiers;
 +import brooklyn.util.text.StringEscapes;
  import brooklyn.util.text.StringFunctions;
  import brooklyn.util.text.Strings;
  


[13/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalancingStrategy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalancingStrategy.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalancingStrategy.java
new file mode 100644
index 0000000..9d3968b
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/BalancingStrategy.java
@@ -0,0 +1,622 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import java.text.MessageFormat;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Represents an abstract algorithm for optimally balancing worker "items" among several "containers" based on the workloads
+ * of the items, and corresponding high- and low-thresholds on the containers.
+ * 
+ * TODO: extract interface, provide default implementation
+ * TODO: remove legacy code comments
+ */
+public class BalancingStrategy<NodeType extends Entity, ItemType extends Movable> {
+
+    // FIXME Bad idea to use MessageFormat.format in this way; if toString of entity contains
+    // special characters interpreted by MessageFormat, then it will all break!
+    
+    // This is a modified version of the watermark elasticity policy from Monterey v3.
+    
+    private static final Logger LOG = LoggerFactory.getLogger(BalancingStrategy.class);
+    
+    private static final int MAX_MIGRATIONS_PER_BALANCING_NODE = 20; // arbitrary (Splodge)
+    private static final boolean BALANCE_COLD_PULLS_IN_SAME_RUN_AS_HOT_PUSHES = false;
+    
+    private final String name;
+    private final BalanceablePoolModel<NodeType, ItemType> model;
+    private final PolicyUtilForPool<NodeType, ItemType> helper;
+//    private boolean loggedColdestTooHigh = false;
+//    private boolean loggedHottestTooLow = false;
+    
+    
+    public BalancingStrategy(String name, BalanceablePoolModel<NodeType, ItemType> model) {
+        this.name = name;
+        this.model = model;
+        this.helper = new PolicyUtilForPool<NodeType, ItemType>(model);
+    }
+    
+    public String getName() {
+        return name;
+    }
+    
+    public void rebalance() {
+        checkAndApplyOn(model.getPoolContents());
+    }
+    
+    public int getMaxMigrationsPerBalancingNode() {
+        return MAX_MIGRATIONS_PER_BALANCING_NODE;
+    }
+    
+    public BalanceablePoolModel<NodeType, ItemType> getDataProvider() {
+        return model;
+    }
+    
+    // This was the entry point for the legacy policy.
+    private void checkAndApplyOn(final Collection<NodeType> dirtyNodesSupplied) {
+        Collection<NodeType> dirtyNodes = dirtyNodesSupplied;
+        
+//        if (startTime + FORCE_ALL_NODES_IF_DELAYED_FOR_MILLIS < System.currentTimeMillis()) {
+//            Set<NodeType> allNodes = new LinkedHashSet<NodeType>();
+//            allNodes.addAll(dirtyNodes);
+//            dirtyNodes = allNodes;
+//            allNodes.addAll(getDataProvider().getPoolContents());
+//            if (LOG.isDebugEnabled())
+//                LOG.debug("policy "+getDataProvider().getAbbr()+" running after delay ("+
+//                        TimeUtils.makeTimeString(System.currentTimeMillis() - startTime)+", analysing all nodes: "+
+//                        dirtyNodes);
+//        }
+        
+//        nodeFinder.optionalCachedNodesWithBacklogDetected.clear();
+//        boolean gonnaGrow = growPool(dirtyNodes);
+//        getDataProvider().waitForAllTransitionsComplete();
+        boolean gonnaGrow = false;
+        
+        Set<NodeType> nonFrozenDirtyNodes = new LinkedHashSet<NodeType>(dirtyNodes);
+//        boolean gonnaShrink = false;
+//        if (!gonnaGrow && !DONT_SHRINK_UNLESS_BALANCED) {
+//            gonnaShrink = shrinkPool(nonFrozenDirtyNodes);
+//            getDataProvider().waitForAllTransitionsComplete();
+//        }
+        
+        if (getDataProvider().getPoolSize() >= 2) {
+            boolean didBalancing = false;
+            for (NodeType a : nonFrozenDirtyNodes) {
+                didBalancing |= balanceItemsOnNodesInQuestion(a, gonnaGrow);
+//                getMutator().waitForAllTransitionsComplete();
+            }
+            if (didBalancing) {
+                return;
+            }
+        }
+        
+//        if (!gonnaGrow && DONT_SHRINK_UNLESS_BALANCED) {
+//            gonnaShrink = shrinkPool(nonFrozenDirtyNodes);
+//            getDataProvider().waitForAllTransitionsComplete();
+//        }
+        
+//        if (gonnaGrow || gonnaShrink)
+//        //don't log 'doing nothing' message
+//        return;
+        
+//        if (LOG.isDebugEnabled()) {
+//            double poolTotal = getDataProvider().getPoolPredictedWorkrateTotal();
+//            int poolSize = getDataProvider().getPoolPredictedSize();
+//            LOG.debug(MessageFormat.format("policy "+getDataProvider().getAbbr()+" did nothing; pool workrate is {0,number,#.##} x {1} nodes",
+//                    1.0*poolTotal/poolSize, poolSize));
+//        }
+    }
+    
+    protected boolean balanceItemsOnNodesInQuestion(NodeType questionedNode, boolean gonnaGrow) {
+        double questionedNodeTotalWorkrate = getDataProvider().getTotalWorkrate(questionedNode);
+        
+        boolean balanced = balanceItemsOnHotNode(questionedNode, questionedNodeTotalWorkrate, gonnaGrow);
+//        getMutator().waitForAllTransitionsComplete();
+        
+        if (!balanced || BALANCE_COLD_PULLS_IN_SAME_RUN_AS_HOT_PUSHES) {
+            balanced |= balanceItemsOnColdNode(questionedNode, questionedNodeTotalWorkrate, gonnaGrow);
+//            getMutator().waitForAllTransitionsComplete();
+        }
+        if (balanced)
+            return true;
+        
+        if (LOG.isDebugEnabled()) {
+            LOG.debug( MessageFormat.format(
+                    "policy "+getDataProvider().getName()+" not balancing "+questionedNode+"; " +
+                    "its workrate {0,number,#.##} is acceptable (or cannot be balanced)", questionedNodeTotalWorkrate) );
+        }
+        return false;
+    }
+    
+    protected boolean balanceItemsOnHotNode(NodeType node, double nodeWorkrate, boolean gonnaGrow) {
+        double originalNodeWorkrate = nodeWorkrate;
+        int migrationCount = 0;
+        int iterationCount = 0;
+        
+        Set<ItemType> itemsMoved = new LinkedHashSet<ItemType>();
+        Set<NodeType> nodesChecked = new LinkedHashSet<NodeType>();
+        
+//        if (nodeFinder.config.COUNT_BACKLOG_AS_EXTRA_WORKRATE) {
+//            int backlog = nodeFinder.getBacklogQueueLength(questionedNode);
+//            if (backlog>0) {
+//                Level l = backlog>1000 ? Level.INFO : backlog>10 ? Level.FINE : Level.FINER;
+//                if (LOG.isLoggable(l)) {
+//                    LOG.log( l, MessageFormat.format(
+//                            "policy "+getDataProvider().getAbbr()+" detected queue at node "+questionedNode+", " +
+//                            "inflating workrate {0,number,#.##} + "+backlog, questionedNodeTotalWorkrate) );
+//                }
+//                questionedNodeTotalWorkrate += backlog;
+//            }
+//        }
+        
+        Double highThreshold = model.getHighThreshold(node);
+        if (highThreshold == -1) {
+            // node presumably has been removed; TODO log
+            return false;
+        }
+        
+        while (nodeWorkrate > highThreshold && migrationCount < getMaxMigrationsPerBalancingNode()) {
+            iterationCount++;
+            
+            if (LOG.isDebugEnabled()) {
+                LOG.debug(MessageFormat.format(
+                        "policy "+getDataProvider().getName()+" considering balancing hot node "+node+" " +
+                        "(workrate {0,number,#.##}); iteration "+iterationCount, nodeWorkrate) );
+            }
+            
+            // move from hot node, to coldest
+            
+            NodeType coldNode = helper.findColdestContainer(nodesChecked);
+            
+            if (coldNode == null) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug( MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" not balancing hot node "+node+" " +
+                            "(workrate {0,number,#.##}); no coldest node available", nodeWorkrate) );
+                }
+                break;
+            }
+            
+            if (coldNode.equals(node)) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug( MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" not balancing hot node "+node+" " +
+                            "(workrate {0,number,#.##}); it is also the coldest modifiable node", nodeWorkrate) );
+                }
+                break;
+            }
+            
+            double coldNodeWorkrate = getDataProvider().getTotalWorkrate(coldNode);
+            boolean emergencyLoadBalancing = coldNodeWorkrate < nodeWorkrate*2/3;
+            double coldNodeHighThreshold = model.getHighThreshold(coldNode);
+            if (coldNodeWorkrate >= coldNodeHighThreshold && !emergencyLoadBalancing) {
+                //don't balance if all nodes are approx equally hot (and very hot)
+                
+                //for now, stop warning if it is a recurring theme!
+//                Level level = loggedColdestTooHigh ? Level.FINER : Level.INFO;
+//                LOG.log(level, MessageFormat.format(
+//                        "policy "+getDataProvider().getAbbr()+" not balancing hot node "+questionedNode+" " +
+//                        "(workrate {0,number,#.##}); coldest node "+coldNode+" has workrate {1,number,#.##} also too high"+
+//                        (loggedColdestTooHigh ? "" : " (future cases will be logged at finer)"),
+//                        questionedNodeTotalWorkrate, coldNodeWorkrate) );
+//                loggedColdestTooHigh = true;
+                break;
+            }
+            double poolLowWatermark = Double.MAX_VALUE; // TODO
+            if (gonnaGrow && (coldNodeWorkrate >= poolLowWatermark && !emergencyLoadBalancing)) {
+                //if we're growing the pool, refuse to balance unless the cold node is indeed very cold, or hot node very hot
+                
+                //for now, stop warning if it is a recurring theme!
+//                Level level = loggedColdestTooHigh ? Level.FINER : Level.INFO;
+//                LOG.log(level, MessageFormat.format(
+//                        "policy "+getDataProvider().getAbbr()+" not balancing hot node "+questionedNode+" " +
+//                        "(workrate {0,number,#.##}); coldest node "+coldNode+" has workrate {1,number,#.##} also too high to accept while pool is growing" +
+//                        (loggedColdestTooHigh ? "" : " (future cases will be logged at finer)"),
+//                        questionedNodeTotalWorkrate, coldNodeWorkrate) );
+//                loggedColdestTooHigh = true;
+                break;
+            }
+            
+            String questionedNodeName = getDataProvider().getName(node);
+            String coldNodeName = getDataProvider().getName(coldNode);
+            Location coldNodeLocation = getDataProvider().getLocation(coldNode);
+            
+            if (LOG.isDebugEnabled()) {
+                LOG.debug( MessageFormat.format(
+                        "policy "+getDataProvider().getName()+" balancing hot node "+questionedNodeName+" " +
+                        "("+node+", workrate {0,number,#.##}), " +
+                        "considering target "+coldNodeName+" ("+coldNode+", workrate {1,number,#.##})",
+                        nodeWorkrate, coldNodeWorkrate) );
+            }
+            
+            double idealSizeToMove = (nodeWorkrate - coldNodeWorkrate) / 2;
+            //if the 'ideal' amount to move would cause cold to be too hot, then reduce ideal amount
+            
+            if (idealSizeToMove + coldNodeWorkrate > coldNodeHighThreshold)
+                idealSizeToMove = coldNodeHighThreshold - coldNodeWorkrate;
+            
+            
+            double maxSizeToMoveIdeally = Math.min(
+                    nodeWorkrate/2 + 0.00001,
+                    //permit it to exceed node high if there is no alternative (this is 'max' not 'ideal'),
+                    //so long as it still gives a significant benefit
+                    //                      getConfiguration().nodeHighWaterMark - coldNodeWorkrate,
+                    (nodeWorkrate - coldNodeWorkrate)*0.9);
+            double maxSizeToMoveIfNoSmallButLarger = nodeWorkrate*3/4;
+            
+            Map<ItemType, Double> questionedNodeItems = getDataProvider().getItemWorkrates(node);
+            if (questionedNodeItems == null) {
+                if (LOG.isDebugEnabled())
+                    LOG.debug(MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" balancing hot node "+questionedNodeName+" " +
+                            "("+node+", workrate {0,number,#.##}), abandoned; " +
+                            "item report for " + questionedNodeName + " unavailable",
+                            nodeWorkrate));
+                break;
+            }
+            ItemType itemToMove = findBestItemToMove(questionedNodeItems, idealSizeToMove, maxSizeToMoveIdeally,
+                    maxSizeToMoveIfNoSmallButLarger, itemsMoved, coldNodeLocation);
+            
+            if (itemToMove == null) {
+                if (LOG.isDebugEnabled())
+                    LOG.debug(MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" balancing hot node "+questionedNodeName+" " +
+                            "("+node+", workrate {0,number,#.##}), ending; " +
+                            "no suitable segment found " +
+                            "(ideal transition item size {1,number,#.##}, max {2,number,#.##}, " +
+                            "moving to coldest node "+coldNodeName+" ("+coldNode+", workrate {3,number,#.##}); available items: {4}",
+                            nodeWorkrate, idealSizeToMove, maxSizeToMoveIdeally, coldNodeWorkrate, questionedNodeItems) );
+                break;
+            }
+            
+            itemsMoved.add(itemToMove);
+            double itemWorkrate = questionedNodeItems.get(itemToMove);
+            
+//            if (LOG.isLoggable(Level.FINE))
+//                LOG.fine( MessageFormat.format(
+//                        "policy "+getDataProvider().getAbbr()+" balancing hot node "+questionedNodeName+" " +
+//                        "(workrate {0,number,#.##}, too high), transitioning " + itemToMove +
+//                        " to "+coldNodeName+" (workrate {1,number,#.##}, now += {2,number,#.##})",
+//                        questionedNodeTotalWorkrate, coldNodeWorkrate, segmentRate) );
+            
+            nodeWorkrate -= itemWorkrate;
+            coldNodeWorkrate += itemWorkrate;
+            
+            moveItem(itemToMove, node, coldNode);
+            ++migrationCount;
+        }
+        
+        if (LOG.isDebugEnabled()) {
+            if (iterationCount == 0) {
+                if (LOG.isTraceEnabled())
+                    LOG.trace( MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" balancing if hot finished at node "+node+"; " +
+                            "workrate {0,number,#.##} not hot",
+                            originalNodeWorkrate) );
+            }
+            else if (itemsMoved.isEmpty()) {
+                if (LOG.isTraceEnabled())
+                    LOG.trace( MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" balancing finished at hot node "+node+" " +
+                            "(workrate {0,number,#.##}); no way to improve it",
+                            originalNodeWorkrate) );
+            } else {
+                LOG.debug( MessageFormat.format(
+                        "policy "+getDataProvider().getName()+" balancing finished at hot node "+node+"; " +
+                        "workrate from {0,number,#.##} to {1,number,#.##} (report now says {2,number,#.##}) " +
+                        "by moving off {3}",
+                        originalNodeWorkrate,
+                        nodeWorkrate,
+                        getDataProvider().getTotalWorkrate(node),
+                        itemsMoved
+                        ) );
+            }
+        }
+        return !itemsMoved.isEmpty();
+    }
+    
+    protected boolean balanceItemsOnColdNode(NodeType questionedNode, double questionedNodeTotalWorkrate, boolean gonnaGrow) {
+        // Abort if the node has pending adjustments.
+        Map<ItemType, Double> items = getDataProvider().getItemWorkrates(questionedNode);
+        if (items == null) {
+            if (LOG.isDebugEnabled()) {
+                LOG.debug( MessageFormat.format(
+                        "policy "+getDataProvider().getName()+" not balancing cold node "+questionedNode+" " +
+                        "(workrate {0,number,#.##}); workrate breakdown unavailable (probably reverting)",
+                        questionedNodeTotalWorkrate) );
+            }
+            return false;
+        }
+        for (ItemType item : items.keySet()) {
+            if (!model.isItemMoveable(item)) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug( MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" not balancing cold node "+questionedNode+" " +
+                            "(workrate {0,number,#.##}); at least one item ("+item+") is in flux",
+                            questionedNodeTotalWorkrate) );
+                }
+                return false;
+            }
+        }
+        
+        double originalQuestionedNodeTotalWorkrate = questionedNodeTotalWorkrate;
+        int numMigrations = 0;
+        
+        Set<ItemType> itemsMoved = new LinkedHashSet<ItemType>();
+        Set<NodeType> nodesChecked = new LinkedHashSet<NodeType>();
+        
+        int iters = 0;
+        Location questionedLocation = getDataProvider().getLocation(questionedNode);
+        
+        double lowThreshold = model.getLowThreshold(questionedNode);
+        while (questionedNodeTotalWorkrate < lowThreshold) {
+            iters++;
+            
+            if (LOG.isDebugEnabled()) {
+                LOG.debug( MessageFormat.format(
+                        "policy "+getDataProvider().getName()+" considering balancing cold node "+questionedNode+" " +
+                        "(workrate {0,number,#.##}); iteration "+iters, questionedNodeTotalWorkrate));
+            }
+            
+            // move from cold node, to hottest
+            
+            NodeType hotNode = helper.findHottestContainer(nodesChecked);
+            
+            if (hotNode == null) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug( MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" not balancing cold node "+questionedNode+" " +
+                            "(workrate {0,number,#.##}); no hottest node available", questionedNodeTotalWorkrate) );
+                }
+                
+                break;
+            }
+            if (hotNode.equals(questionedNode)) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug( MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" not balancing cold node "+questionedNode+" " +
+                            "(workrate {0,number,#.##}); it is also the hottest modfiable node", questionedNodeTotalWorkrate) );
+                }
+                break;
+            }
+            
+            
+            double hotNodeWorkrate = getDataProvider().getTotalWorkrate(hotNode);
+            double hotNodeLowThreshold = model.getLowThreshold(hotNode);
+            double hotNodeHighThreshold = model.getHighThreshold(hotNode);
+            boolean emergencyLoadBalancing = false;  //doesn't apply to cold
+            if (hotNodeWorkrate == -1 || hotNodeLowThreshold == -1 || hotNodeHighThreshold == -1) {
+                // hotNode presumably has been removed; TODO log
+                break;
+            }
+            if (hotNodeWorkrate <= hotNodeLowThreshold && !emergencyLoadBalancing) {
+                //don't balance if all nodes are too low
+                
+                //for now, stop warning if it is a recurring theme!
+//                Level level = loggedHottestTooLow ? Level.FINER : Level.INFO;
+//                LOG.log(level, MessageFormat.format(
+//                        "policy "+getDataProvider().getAbbr()+" not balancing cold node "+questionedNode+" " +
+//                        "(workrate {0,number,#.##}); hottest node "+hotNode+" has workrate {1,number,#.##} also too low" +
+//                        (loggedHottestTooLow ? "" : " (future cases will be logged at finer)"),
+//                        questionedNodeTotalWorkrate, hotNodeWorkrate) );
+//                loggedHottestTooLow = true;
+                break;
+            }
+            if (gonnaGrow && (hotNodeWorkrate <= hotNodeHighThreshold && !emergencyLoadBalancing)) {
+                //if we're growing the pool, refuse to balance unless the hot node is quite hot
+                
+                //again, stop warning if it is a recurring theme!
+//                Level level = loggedHottestTooLow ? Level.FINER : Level.INFO;
+//                LOG.log(level, MessageFormat.format(
+//                        "policy "+getDataProvider().getAbbr()+" not balancing cold node "+questionedNode+" " +
+//                        "(workrate {0,number,#.##}); hottest node "+hotNode+" has workrate {1,number,#.##} also too low to accept while pool is growing"+
+//                        (loggedHottestTooLow ? "" : " (future cases will be logged at finer)"),
+//                        questionedNodeTotalWorkrate, hotNodeWorkrate) );
+//                loggedHottestTooLow = true;
+                break;
+            }
+            
+            String questionedNodeName = getDataProvider().getName(questionedNode);
+            String hotNodeName = getDataProvider().getName(hotNode);
+            
+            if (LOG.isDebugEnabled()) {
+                LOG.debug( MessageFormat.format(
+                        "policy "+getDataProvider().getName()+" balancing cold node "+questionedNodeName+" " +
+                        "("+questionedNode+", workrate {0,number,#.##}), " +
+                        "considering source "+hotNodeName+" ("+hotNode+", workrate {1,number,#.##})",
+                        questionedNodeTotalWorkrate, hotNodeWorkrate) );
+            }
+            
+            double idealSizeToMove = (hotNodeWorkrate - questionedNodeTotalWorkrate) / 2;
+            //if the 'ideal' amount to move would cause cold to be too hot, then reduce ideal amount
+            double targetNodeHighThreshold = model.getHighThreshold(questionedNode);
+            if (idealSizeToMove + questionedNodeTotalWorkrate > targetNodeHighThreshold)
+                idealSizeToMove = targetNodeHighThreshold - questionedNodeTotalWorkrate;
+            double maxSizeToMoveIdeally = Math.min(
+                    hotNodeWorkrate/2,
+                    //allow to swap order, but not very much (0.9 was allowed when balancing high)
+                    (hotNodeWorkrate - questionedNodeTotalWorkrate)*0.6);
+            double maxSizeToMoveIfNoSmallButLarger = questionedNodeTotalWorkrate*3/4;
+            
+            Map<ItemType, Double> hotNodeItems = getDataProvider().getItemWorkrates(hotNode);
+            if (hotNodeItems == null) {
+                if (LOG.isDebugEnabled())
+                    LOG.debug(MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" balancing cold node "+questionedNodeName+" " +
+                            "("+questionedNode+", workrate {0,number,#.##}), " +
+                            "excluding hot node "+hotNodeName+" because its item report unavailable",
+                            questionedNodeTotalWorkrate));
+                nodesChecked.add(hotNode);
+                continue;
+            }
+            
+            ItemType itemToMove = findBestItemToMove(hotNodeItems, idealSizeToMove, maxSizeToMoveIdeally,
+                    maxSizeToMoveIfNoSmallButLarger, itemsMoved, questionedLocation);
+            if (itemToMove == null) {
+                if (LOG.isDebugEnabled())
+                    LOG.debug(MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" balancing cold node "+questionedNodeName+" " +
+                            "("+questionedNode+", workrate {0,number,#.##}), " +
+                            "excluding hot node "+hotNodeName+" because it has no appilcable items " +
+                            "(ideal transition item size {1,number,#.##}, max {2,number,#.##}, " +
+                            "moving from hot node "+hotNodeName+" ("+hotNode+", workrate {3,number,#.##}); available items: {4}",
+                            questionedNodeTotalWorkrate, idealSizeToMove, maxSizeToMoveIdeally, hotNodeWorkrate, hotNodeItems) );
+                
+                nodesChecked.add(hotNode);
+                continue;
+            }
+            
+            itemsMoved.add(itemToMove);
+            double segmentRate = hotNodeItems.get(itemToMove);
+            
+//            if (LOG.isLoggable(Level.FINE))
+//                LOG.fine( MessageFormat.format(
+//                        "policy "+getDataProvider().getAbbr()+" balancing cold node "+questionedNodeName+" " +
+//                        "(workrate {0,number,#.##}, too low), transitioning " + itemToMove +
+//                        " from "+hotNodeName+" (workrate {1,number,#.##}, now -= {2,number,#.##})",
+//                        questionedNodeTotalWorkrate, hotNodeWorkrate, segmentRate) );
+            
+            questionedNodeTotalWorkrate += segmentRate;
+            hotNodeWorkrate -= segmentRate;
+            
+            moveItem(itemToMove, hotNode, questionedNode);
+            
+            if (++numMigrations >= getMaxMigrationsPerBalancingNode()) {
+                break;
+            }
+        }
+        
+        if (LOG.isDebugEnabled()) {
+            if (iters == 0) {
+                if (LOG.isTraceEnabled())
+                    LOG.trace( MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" balancing if cold finished at node "+questionedNode+"; " +
+                            "workrate {0,number,#.##} not cold",
+                            originalQuestionedNodeTotalWorkrate) );
+            }
+            else if (itemsMoved.isEmpty()) {
+                if (LOG.isTraceEnabled())
+                    LOG.trace( MessageFormat.format(
+                            "policy "+getDataProvider().getName()+" balancing finished at cold node "+questionedNode+" " +
+                            "(workrate {0,number,#.##}); no way to improve it",
+                            originalQuestionedNodeTotalWorkrate) );
+            } else {
+                LOG.debug( MessageFormat.format(
+                        "policy "+getDataProvider().getName()+" balancing finished at cold node "+questionedNode+"; " +
+                        "workrate from {0,number,#.##} to {1,number,#.##} (report now says {2,number,#.##}) " +
+                        "by moving in {3}",
+                        originalQuestionedNodeTotalWorkrate,
+                        questionedNodeTotalWorkrate,
+                        getDataProvider().getTotalWorkrate(questionedNode),
+                        itemsMoved) );
+            }
+        }
+        return !itemsMoved.isEmpty();
+    }
+    
+    protected void moveItem(ItemType item, NodeType oldNode, NodeType newNode) {
+        item.move(newNode);
+        model.onItemMoved(item, newNode);
+    }
+    
+    /**
+     * "Best" is defined as nearest to the targetCost, without exceeding maxCost, unless maxCostIfNothingSmallerButLarger > 0
+     * which does just that (useful if the ideal and target are estimates and aren't quite right, typically it will take
+     * something larger than maxRate but less than half the total rate, which is only possible when the estimates don't agree)
+     */
+    protected ItemType findBestItemToMove(Map<ItemType, Double> costsPerItem, double targetCost, double maxCost,
+            double maxCostIfNothingSmallerButLarger, Set<ItemType> excludedItems, Location locationIfKnown) {
+        
+        ItemType closestMatch = null;
+        ItemType smallestMoveable = null, largest = null;
+        double minDiff = Double.MAX_VALUE, smallestC = Double.MAX_VALUE, largestC = Double.MIN_VALUE;
+        boolean exclusions = false;
+        
+        for (Entry<ItemType, Double> entry : costsPerItem.entrySet()) {
+            ItemType item = entry.getKey();
+            Double cost = entry.getValue();
+            
+            if (cost == null) {
+                if (LOG.isDebugEnabled()) LOG.debug(MessageFormat.format("Item ''{0}'' has null workrate: skipping", item));
+                continue;
+            }
+            
+            if (!model.isItemMoveable(item)) {
+                if (LOG.isDebugEnabled()) LOG.debug(MessageFormat.format("Item ''{0}'' cannot be moved: skipping", item));
+                continue;
+            }
+            if (cost < 0) {
+                if (LOG.isDebugEnabled()) LOG.debug(MessageFormat.format("Item ''{0}'' subject to recent adjustment: skipping", item));
+                continue;
+            }
+            if (excludedItems.contains(item)) {
+                exclusions = true;
+                continue;
+            }
+            if (cost < 0) { // FIXME: already tested above
+                exclusions = true;
+                continue;
+            }
+            if (cost <= 0) { // FIXME: overlaps previous condition
+                continue;
+            }
+            if (largest == null || cost > largestC) {
+                largest = item;
+                largestC = cost;
+            }
+            if (!model.isItemMoveable(item)) { // FIXME: already tested above
+                continue;
+            }
+            if (locationIfKnown != null && !model.isItemAllowedIn(item, locationIfKnown)) {
+                continue;
+            }
+            if (smallestMoveable == null || cost < smallestC) {
+                smallestMoveable = item;
+                smallestC = cost;
+            }
+            if (cost > maxCost) {
+                continue;
+            }
+            double diff = Math.abs(targetCost - cost);
+            if (closestMatch == null || diff < minDiff) {
+                closestMatch = item;
+                minDiff = diff;
+            }
+        }
+        
+        if (closestMatch != null)
+            return closestMatch;
+        
+        if (smallestC < maxCostIfNothingSmallerButLarger && smallestC < largestC && !exclusions)
+            return smallestMoveable;
+        
+        return null;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/DefaultBalanceablePoolModel.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/DefaultBalanceablePoolModel.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/DefaultBalanceablePoolModel.java
new file mode 100644
index 0000000..5f6cabc
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/DefaultBalanceablePoolModel.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.api.location.Location;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
+
+/**
+ * Standard implementation of {@link BalanceablePoolModel}, providing essential arithmetic for item and container
+ * workrates and thresholds. See subclasses for specific requirements for migrating items.
+ */
+public class DefaultBalanceablePoolModel<ContainerType, ItemType> implements BalanceablePoolModel<ContainerType, ItemType> {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultBalanceablePoolModel.class);
+    
+    /*
+     * Performance comments.
+     *  - Used hprof with LoadBalancingPolicySoakTest.testLoadBalancingManyManyItemsTest (1000 items)
+     *  - Prior to adding containerToItems, it created a new set by iterating over all items.
+     *    This was the biggest percentage of any brooklyn code.
+     *    Hence it's worth duplicating the values, keyed by item and keyed by container.
+     *  - Unfortunately changing threading model (so have a "rebalancer" thread, and a thread that 
+     *    processes events to update the model), get ConcurrentModificationException if don't take
+     *    copy of containerToItems.get(node)...
+     */
+    
+    // Concurrent maps cannot have null value; use this to represent when no container is supplied for an item 
+    private static final String NULL_CONTAINER = "null-container";
+    
+    private final String name;
+    private final Set<ContainerType> containers = Collections.newSetFromMap(new ConcurrentHashMap<ContainerType,Boolean>());
+    private final Map<ContainerType, Double> containerToLowThreshold = new ConcurrentHashMap<ContainerType, Double>();
+    private final Map<ContainerType, Double> containerToHighThreshold = new ConcurrentHashMap<ContainerType, Double>();
+    private final Map<ItemType, ContainerType> itemToContainer = new ConcurrentHashMap<ItemType, ContainerType>();
+    private final SetMultimap<ContainerType, ItemType> containerToItems =  Multimaps.synchronizedSetMultimap(HashMultimap.<ContainerType, ItemType>create());
+    private final Map<ItemType, Double> itemToWorkrate = new ConcurrentHashMap<ItemType, Double>();
+    private final Set<ItemType> immovableItems = Collections.newSetFromMap(new ConcurrentHashMap<ItemType, Boolean>());
+    
+    private volatile double poolLowThreshold = 0;
+    private volatile double poolHighThreshold = 0;
+    private volatile double currentPoolWorkrate = 0;
+    
+    public DefaultBalanceablePoolModel(String name) {
+        this.name = name;
+    }
+    
+    public ContainerType getParentContainer(ItemType item) {
+        ContainerType result = itemToContainer.get(item);
+        return (result != NULL_CONTAINER) ? result : null;
+    }
+    
+    public Set<ItemType> getItemsForContainer(ContainerType node) {
+        Set<ItemType> result = containerToItems.get(node);
+        synchronized (containerToItems) {
+            return (result != null) ? ImmutableSet.copyOf(result) : Collections.<ItemType>emptySet();
+        }
+    }
+    
+    public Double getItemWorkrate(ItemType item) {
+        return itemToWorkrate.get(item);
+    }
+    
+    @Override public double getPoolLowThreshold() { return poolLowThreshold; }
+    @Override public double getPoolHighThreshold() { return poolHighThreshold; }
+    @Override public double getCurrentPoolWorkrate() { return currentPoolWorkrate; }
+    @Override public boolean isHot() { return !containers.isEmpty() && currentPoolWorkrate > poolHighThreshold; }
+    @Override public boolean isCold() { return !containers.isEmpty() && currentPoolWorkrate < poolLowThreshold; }
+    
+    
+    // Provider methods.
+    
+    @Override public String getName() { return name; }
+    @Override public int getPoolSize() { return containers.size(); }
+    @Override public Set<ContainerType> getPoolContents() { return containers; }
+    @Override public String getName(ContainerType container) { return container.toString(); } // TODO: delete?
+    @Override public Location getLocation(ContainerType container) { return null; } // TODO?
+    
+    @Override public double getLowThreshold(ContainerType container) {
+        Double result = containerToLowThreshold.get(container);
+        return (result != null) ? result : -1;
+    }
+    
+    @Override public double getHighThreshold(ContainerType container) {
+        Double result = containerToHighThreshold.get(container);
+        return (result != null) ? result : -1;
+    }
+    
+    @Override public double getTotalWorkrate(ContainerType container) {
+        double totalWorkrate = 0;
+        for (ItemType item : getItemsForContainer(container)) {
+            Double workrate = itemToWorkrate.get(item);
+            if (workrate != null)
+                totalWorkrate += Math.abs(workrate);
+        }
+        return totalWorkrate;
+    }
+    
+    @Override public Map<ContainerType, Double> getContainerWorkrates() {
+        Map<ContainerType, Double> result = new LinkedHashMap<ContainerType, Double>();
+        for (ContainerType node : containers)
+            result.put(node, getTotalWorkrate(node));
+        return result;
+    }
+    
+    @Override public Map<ItemType, Double> getItemWorkrates(ContainerType node) {
+        Map<ItemType, Double> result = new LinkedHashMap<ItemType, Double>();
+        for (ItemType item : getItemsForContainer(node))
+            result.put(item, itemToWorkrate.get(item));
+        return result;
+    }
+    
+    @Override public boolean isItemMoveable(ItemType item) {
+        // If don't know about item, then assume not movable; otherwise has this item been explicitly flagged as immovable?
+        return itemToContainer.containsKey(item) && !immovableItems.contains(item);
+    }
+    
+    @Override public boolean isItemAllowedIn(ItemType item, Location location) {
+        return true; // TODO?
+    }
+    
+    
+    // Mutators.
+    
+    @Override
+    public void onItemMoved(ItemType item, ContainerType newNode) {
+        if (!itemToContainer.containsKey(item)) {
+            // Item may have been deleted; order of events received from different sources 
+            // (i.e. item itself and for itemGroup membership) is non-deterministic.
+            LOG.info("Balanceable pool model ignoring onItemMoved for unknown item {} to container {}; " +
+                    "if onItemAdded subsequently received will get new container then", item, newNode);
+            return;
+        }
+        ContainerType newNodeNonNull = toNonNullContainer(newNode);
+        ContainerType oldNode = itemToContainer.put(item, newNodeNonNull);
+        if (oldNode != null && oldNode != NULL_CONTAINER) containerToItems.remove(oldNode, item);
+        if (newNode != null) containerToItems.put(newNode, item);
+    }
+    
+    @Override
+    public void onContainerAdded(ContainerType newContainer, double lowThreshold, double highThreshold) {
+        boolean added = containers.add(newContainer);
+        if (!added) {
+            // See LoadBalancingPolicy.onContainerAdded for possible explanation of why can get duplicate calls
+            LOG.debug("Duplicate container-added event for {}; ignoring", newContainer);
+            return;
+        }
+        containerToLowThreshold.put(newContainer, lowThreshold);
+        containerToHighThreshold.put(newContainer, highThreshold);
+        poolLowThreshold += lowThreshold;
+        poolHighThreshold += highThreshold;
+    }
+    
+    @Override
+    public void onContainerRemoved(ContainerType oldContainer) {
+        containers.remove(oldContainer);
+        Double containerLowThreshold = containerToLowThreshold.remove(oldContainer);
+        Double containerHighThresold = containerToHighThreshold.remove(oldContainer);
+        poolLowThreshold -= (containerLowThreshold != null ? containerLowThreshold : 0);
+        poolHighThreshold -= (containerHighThresold != null ? containerHighThresold : 0);
+        
+        // TODO: assert no orphaned items
+    }
+    
+    @Override
+    public void onItemAdded(ItemType item, ContainerType parentContainer) {
+        onItemAdded(item, parentContainer, false);
+    }
+    
+    @Override
+    public void onItemAdded(ItemType item, ContainerType parentContainer, boolean immovable) {
+        // Duplicate calls to onItemAdded do no harm, as long as most recent is most accurate!
+        // Important that it stays that way for now - See LoadBalancingPolicy.onContainerAdded for explanation.
+
+        if (immovable)
+            immovableItems.add(item);
+        
+        ContainerType parentContainerNonNull = toNonNullContainer(parentContainer);
+        ContainerType oldNode = itemToContainer.put(item, parentContainerNonNull);
+        if (oldNode != null && oldNode != NULL_CONTAINER) containerToItems.remove(oldNode, item);
+        if (parentContainer != null) containerToItems.put(parentContainer, item);
+    }
+    
+    @Override
+    public void onItemRemoved(ItemType item) {
+        ContainerType oldNode = itemToContainer.remove(item);
+        if (oldNode != null && oldNode != NULL_CONTAINER) containerToItems.remove(oldNode, item);
+        Double workrate = itemToWorkrate.remove(item);
+        if (workrate != null)
+            currentPoolWorkrate -= workrate;
+        immovableItems.remove(item);
+    }
+    
+    @Override
+    public void onItemWorkrateUpdated(ItemType item, double newValue) {
+        if (hasItem(item)) {
+            Double oldValue = itemToWorkrate.put(item, newValue);
+            double delta = ( newValue - (oldValue != null ? oldValue : 0) );
+            currentPoolWorkrate += delta;
+        } else {
+            // Can happen when item removed - get notification of removal and workrate from group and item
+            // respectively, so can overtake each other
+            if (LOG.isDebugEnabled()) LOG.debug("Ignoring setting of workrate for unknown item {}, to {}", item, newValue);
+        }
+    }
+    
+    private boolean hasItem(ItemType item) {
+        return itemToContainer.containsKey(item);
+    }
+    
+    
+    // Additional methods for tests.
+
+    /**
+     * Warning: this can be an expensive (time and memory) operation if there are a lot of items/containers. 
+     */
+    @VisibleForTesting
+    public String itemDistributionToString() {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        dumpItemDistribution(new PrintStream(baos));
+        return new String(baos.toByteArray());
+    }
+
+    @VisibleForTesting
+    public void dumpItemDistribution() {
+        dumpItemDistribution(System.out);
+    }
+    
+    @VisibleForTesting
+    public void dumpItemDistribution(PrintStream out) {
+        for (ContainerType container : getPoolContents()) {
+            out.println("Container '"+container+"': ");
+            for (ItemType item : getItemsForContainer(container)) {
+                Double workrate = getItemWorkrate(item);
+                out.println("\t"+"Item '"+item+"' ("+workrate+")");
+            }
+        }
+        out.flush();
+    }
+    
+    @SuppressWarnings("unchecked")
+    private ContainerType nullContainer() {
+        return (ContainerType) NULL_CONTAINER; // relies on erasure
+    }
+    
+    private ContainerType toNonNullContainer(ContainerType container) {
+        return (container != null) ? container : nullContainer();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroup.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroup.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroup.java
new file mode 100644
index 0000000..2538bc6
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroup.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.proxying.ImplementedBy;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.DynamicGroup;
+import brooklyn.event.basic.BasicConfigKey;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+
+/**
+ * A group of items that are contained within a given (dynamically changing) set of containers.
+ * 
+ * The {@link setContainers(Group)} sets the group of containers. The membership of that group
+ * is dynamically tracked.
+ * 
+ * When containers are added/removed, or when an items is added/removed, or when an {@link Moveable} item 
+ * is moved then the membership of this group of items is automatically updated accordingly.
+ * 
+ * For example: in Monterey, this could be used to track the actors that are within a given cluster of venues.
+ */
+@ImplementedBy(ItemsInContainersGroupImpl.class)
+public interface ItemsInContainersGroup extends DynamicGroup {
+
+    @SetFromFlag("itemFilter")
+    public static final ConfigKey<Predicate<? super Entity>> ITEM_FILTER = new BasicConfigKey(
+            Predicate.class, "itemsInContainerGroup.itemFilter", "Filter for which items within the containers will automatically be in group", Predicates.alwaysTrue());
+
+    public void setContainers(Group containerGroup);
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroupImpl.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroupImpl.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroupImpl.java
new file mode 100644
index 0000000..225a2b6
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/ItemsInContainersGroupImpl.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.AbstractGroup;
+import brooklyn.entity.basic.DynamicGroupImpl;
+
+import com.google.common.base.Predicate;
+
+/**
+ * A group of items that are contained within a given (dynamically changing) set of containers.
+ * 
+ * The {@link setContainers(Group)} sets the group of containers. The membership of that group
+ * is dynamically tracked.
+ * 
+ * When containers are added/removed, or when an items is added/removed, or when an {@link Moveable} item 
+ * is moved then the membership of this group of items is automatically updated accordingly.
+ * 
+ * For example: in Monterey, this could be used to track the actors that are within a given cluster of venues.
+ */
+public class ItemsInContainersGroupImpl extends DynamicGroupImpl implements ItemsInContainersGroup {
+
+    // TODO Inefficient: will not scale to many 1000s of items
+
+    private static final Logger LOG = LoggerFactory.getLogger(ItemsInContainersGroup.class);
+    
+    private Group containerGroup;
+    
+    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
+        @Override
+        public void onEvent(SensorEvent<Object> event) {
+            Entity source = event.getSource();
+            Object value = event.getValue();
+            Sensor sensor = event.getSensor();
+            
+            if (sensor.equals(AbstractGroup.MEMBER_ADDED)) {
+                onContainerAdded((Entity) value);
+            } else if (sensor.equals(AbstractGroup.MEMBER_REMOVED)) {
+                onContainerRemoved((Entity) value);
+            } else if (sensor.equals(Movable.CONTAINER)) {
+                onItemMoved((Movable)source, (BalanceableContainer<?>) value);
+            } else {
+                throw new IllegalStateException("Unhandled event type "+sensor+": "+event);
+            }
+        }
+    };
+
+    public ItemsInContainersGroupImpl() {
+    }
+    
+    @Override
+    public void init() {
+        super.init();
+        setEntityFilter(new Predicate<Entity>() {
+            @Override public boolean apply(Entity e) {
+                return acceptsEntity(e);
+            }});
+    }
+    
+    protected Predicate<? super Entity> getItemFilter() {
+        return getConfig(ITEM_FILTER);
+    }
+    
+    @Override
+    protected boolean acceptsEntity(Entity e) {
+        if (e instanceof Movable) {
+            return acceptsItem((Movable)e, ((Movable)e).getAttribute(Movable.CONTAINER));
+        }
+        return false;
+    }
+
+    boolean acceptsItem(Movable e, BalanceableContainer c) {
+        return (containerGroup != null && c != null) ? getItemFilter().apply(e) && containerGroup.hasMember(c) : false;
+    }
+
+    @Override
+    public void setContainers(Group containerGroup) {
+        this.containerGroup = containerGroup;
+        subscribe(containerGroup, AbstractGroup.MEMBER_ADDED, eventHandler);
+        subscribe(containerGroup, AbstractGroup.MEMBER_REMOVED, eventHandler);
+        subscribe(null, Movable.CONTAINER, eventHandler);
+        
+        if (LOG.isTraceEnabled()) LOG.trace("{} scanning entities on container group set", this);
+        rescanEntities();
+    }
+    
+    private void onContainerAdded(Entity newContainer) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} rescanning entities on container {} added", this, newContainer);
+        rescanEntities();
+    }
+    
+    private void onContainerRemoved(Entity oldContainer) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} rescanning entities on container {} removed", this, oldContainer);
+        rescanEntities();
+    }
+    
+    protected void onEntityAdded(Entity item) {
+        if (acceptsEntity(item)) {
+            if (LOG.isDebugEnabled()) LOG.debug("{} adding new item {}", this, item);
+            addMember(item);
+        }
+    }
+    
+    protected void onEntityRemoved(Entity item) {
+        if (removeMember(item)) {
+            if (LOG.isDebugEnabled()) LOG.debug("{} removing deleted item {}", this, item);
+        }
+    }
+    
+    private void onItemMoved(Movable item, BalanceableContainer container) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} processing moved item {}, to container {}", new Object[] {this, item, container});
+        if (hasMember(item)) {
+            if (!acceptsItem(item, container)) {
+                if (LOG.isDebugEnabled()) LOG.debug("{} removing moved item {} from group, as new container {} is not a member", new Object[] {this, item, container});
+                removeMember(item);
+            }
+        } else {
+            if (acceptsItem(item, container)) {
+                if (LOG.isDebugEnabled()) LOG.debug("{} adding moved item {} to group, as new container {} is a member", new Object[] {this, item, container});
+                addMember(item);
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicy.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicy.java
new file mode 100644
index 0000000..f437fc8
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/LoadBalancingPolicy.java
@@ -0,0 +1,344 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import static brooklyn.util.JavaGroovyEquivalents.elvis;
+import static brooklyn.util.JavaGroovyEquivalents.groovyTruth;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.event.AttributeSensor;
+import org.apache.brooklyn.api.event.Sensor;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.EntityInternal;
+
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
+
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+
+/**
+ * <p>Policy that is attached to a pool of "containers", each of which can host one or more migratable "items".
+ * The policy monitors the workrates of the items and effects migrations in an attempt to ensure that the containers
+ * are all sufficiently utilized without any of them being overloaded.
+ * 
+ * <p>The particular sensor that defines the items' workrates is specified when the policy is constructed. High- and
+ * low-thresholds are defined as <strong>configuration keys</strong> on each of the container entities in the pool:
+ * for an item sensor named {@code foo.bar.sensorName}, the corresponding container config keys would be named
+ * {@code foo.bar.sensorName.threshold.low} and {@code foo.bar.sensorName.threshold.high}.
+ * 
+ * <p>In addition to balancing items among the available containers, this policy causes the pool Entity to emit
+ * {@code POOL_COLD} and {@code POOL_HOT} events when it is determined that there is a surplus or shortfall
+ * of container resource in the pool respectively. These events may be consumed by a separate policy that is capable
+ * of resizing the container pool.
+ */
+    // removed from catalog because it cannot currently be configured via catalog mechanisms
+    // PolicySpec.create fails due to no no-arg constructor
+    // TODO make metric and model things which can be initialized from config then reinstate in catalog
+//@Catalog(name="Load Balancer", description="Policy that is attached to a pool of \"containers\", each of which "
+//        + "can host one or more migratable \"items\". The policy monitors the workrates of the items and effects "
+//        + "migrations in an attempt to ensure that the containers are all sufficiently utilized without any of "
+//        + "them being overloaded.")
+public class LoadBalancingPolicy<NodeType extends Entity, ItemType extends Movable> extends AbstractPolicy {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(LoadBalancingPolicy.class);
+    
+    @SetFromFlag(defaultVal="100")
+    private long minPeriodBetweenExecs;
+    
+    private final AttributeSensor<? extends Number> metric;
+    private final String lowThresholdConfigKeyName;
+    private final String highThresholdConfigKeyName;
+    private final BalanceablePoolModel<NodeType, ItemType> model;
+    private final BalancingStrategy<NodeType, ItemType> strategy;
+    private BalanceableWorkerPool poolEntity;
+    
+    private volatile ScheduledExecutorService executor;
+    private final AtomicBoolean executorQueued = new AtomicBoolean(false);
+    private volatile long executorTime = 0;
+
+    private int lastEmittedDesiredPoolSize = 0;
+    private static enum TemperatureStates { COLD, HOT }
+    private TemperatureStates lastEmittedPoolTemperature = null; // "cold" or "hot"
+    
+    private final SensorEventListener<Object> eventHandler = new SensorEventListener<Object>() {
+        @SuppressWarnings({ "rawtypes", "unchecked" })
+        public void onEvent(SensorEvent<Object> event) {
+            if (LOG.isTraceEnabled()) LOG.trace("{} received event {}", LoadBalancingPolicy.this, event);
+            Entity source = event.getSource();
+            Object value = event.getValue();
+            Sensor sensor = event.getSensor();
+            
+            if (sensor.equals(metric)) {
+                onItemMetricUpdate((ItemType)source, ((Number) value).doubleValue(), true);
+            } else if (sensor.equals(BalanceableWorkerPool.CONTAINER_ADDED)) {
+                onContainerAdded((NodeType) value, true);
+            } else if (sensor.equals(BalanceableWorkerPool.CONTAINER_REMOVED)) {
+                onContainerRemoved((NodeType) value, true);
+            } else if (sensor.equals(BalanceableWorkerPool.ITEM_ADDED)) {
+                BalanceableWorkerPool.ContainerItemPair pair = (BalanceableWorkerPool.ContainerItemPair) value;
+                onItemAdded((ItemType)pair.item, (NodeType)pair.container, true);
+            } else if (sensor.equals(BalanceableWorkerPool.ITEM_REMOVED)) {
+                BalanceableWorkerPool.ContainerItemPair pair = (BalanceableWorkerPool.ContainerItemPair) value;
+                onItemRemoved((ItemType)pair.item, (NodeType)pair.container, true);
+            } else if (sensor.equals(BalanceableWorkerPool.ITEM_MOVED)) {
+                BalanceableWorkerPool.ContainerItemPair pair = (BalanceableWorkerPool.ContainerItemPair) value;
+                onItemMoved((ItemType)pair.item, (NodeType)pair.container, true);
+            }
+        }
+    };
+
+    public LoadBalancingPolicy() {
+        this(null, null);
+    }
+    
+    public LoadBalancingPolicy(AttributeSensor<? extends Number> metric,
+            BalanceablePoolModel<NodeType, ItemType> model) {
+        this(MutableMap.of(), metric, model);
+    }
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public LoadBalancingPolicy(Map props, AttributeSensor<? extends Number> metric,
+            BalanceablePoolModel<NodeType, ItemType> model) {
+        
+        super(props);
+        this.metric = metric;
+        this.lowThresholdConfigKeyName = metric.getName()+".threshold.low";
+        this.highThresholdConfigKeyName = metric.getName()+".threshold.high";
+        this.model = model;
+        this.strategy = new BalancingStrategy(getDisplayName(), model); // TODO: extract interface, inject impl
+        
+        // TODO Should re-use the execution manager's thread pool, somehow
+        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
+    }
+    
+    @SuppressWarnings("unchecked")
+    @Override
+    public void setEntity(EntityLocal entity) {
+        Preconditions.checkArgument(entity instanceof BalanceableWorkerPool, "Provided entity must be a BalanceableWorkerPool");
+        super.setEntity(entity);
+        this.poolEntity = (BalanceableWorkerPool) entity;
+        
+        // Detect when containers are added to or removed from the pool.
+        subscribe(poolEntity, BalanceableWorkerPool.CONTAINER_ADDED, eventHandler);
+        subscribe(poolEntity, BalanceableWorkerPool.CONTAINER_REMOVED, eventHandler);
+        subscribe(poolEntity, BalanceableWorkerPool.ITEM_ADDED, eventHandler);
+        subscribe(poolEntity, BalanceableWorkerPool.ITEM_REMOVED, eventHandler);
+        subscribe(poolEntity, BalanceableWorkerPool.ITEM_MOVED, eventHandler);
+        
+        // Take heed of any extant containers.
+        for (Entity container : poolEntity.getContainerGroup().getMembers()) {
+            onContainerAdded((NodeType)container, false);
+        }
+        for (Entity item : poolEntity.getItemGroup().getMembers()) {
+            onItemAdded((ItemType)item, (NodeType)item.getAttribute(Movable.CONTAINER), false);
+        }
+
+        scheduleRebalance();
+    }
+    
+    @Override
+    public void suspend() {
+        // TODO unsubscribe from everything? And resubscribe on resume?
+        super.suspend();
+        if (executor != null) executor.shutdownNow();;
+        executorQueued.set(false);
+    }
+    
+    @Override
+    public void resume() {
+        super.resume();
+        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
+        executorTime = 0;
+        executorQueued.set(false);
+    }
+    
+    private ThreadFactory newThreadFactory() {
+        return new ThreadFactoryBuilder()
+                .setNameFormat("brooklyn-followthesunpolicy-%d")
+                .build();
+    }
+
+    private void scheduleRebalance() {
+        if (isRunning() && executorQueued.compareAndSet(false, true)) {
+            long now = System.currentTimeMillis();
+            long delay = Math.max(0, (executorTime + minPeriodBetweenExecs) - now);
+            
+            executor.schedule(new Runnable() {
+                @SuppressWarnings("rawtypes")
+                public void run() {
+                    try {
+                        executorTime = System.currentTimeMillis();
+                        executorQueued.set(false);
+                        strategy.rebalance();
+
+                        if (LOG.isDebugEnabled()) LOG.debug("{} post-rebalance: poolSize={}; workrate={}; lowThreshold={}; " + 
+                                "highThreshold={}", new Object[] {this, model.getPoolSize(), model.getCurrentPoolWorkrate(), 
+                                model.getPoolLowThreshold(), model.getPoolHighThreshold()});
+                        
+                        if (model.isCold()) {
+                            Map eventVal = ImmutableMap.of(
+                                    AutoScalerPolicy.POOL_CURRENT_SIZE_KEY, model.getPoolSize(),
+                                    AutoScalerPolicy.POOL_CURRENT_WORKRATE_KEY, model.getCurrentPoolWorkrate(),
+                                    AutoScalerPolicy.POOL_LOW_THRESHOLD_KEY, model.getPoolLowThreshold(),
+                                    AutoScalerPolicy.POOL_HIGH_THRESHOLD_KEY, model.getPoolHighThreshold());
+            
+                            ((EntityLocal)poolEntity).emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, eventVal);
+                            
+                            if (LOG.isInfoEnabled()) {
+                                int desiredPoolSize = (int) Math.ceil(model.getCurrentPoolWorkrate() / (model.getPoolLowThreshold()/model.getPoolSize()));
+                                if (desiredPoolSize != lastEmittedDesiredPoolSize || lastEmittedPoolTemperature != TemperatureStates.COLD) {
+                                    LOG.info("{} emitted COLD (suggesting {}): {}", new Object[] {this, desiredPoolSize, eventVal});
+                                    lastEmittedDesiredPoolSize = desiredPoolSize;
+                                    lastEmittedPoolTemperature = TemperatureStates.COLD;
+                                }
+                            }
+                        
+                        } else if (model.isHot()) {
+                            Map eventVal = ImmutableMap.of(
+                                    AutoScalerPolicy.POOL_CURRENT_SIZE_KEY, model.getPoolSize(),
+                                    AutoScalerPolicy.POOL_CURRENT_WORKRATE_KEY, model.getCurrentPoolWorkrate(),
+                                    AutoScalerPolicy.POOL_LOW_THRESHOLD_KEY, model.getPoolLowThreshold(),
+                                    AutoScalerPolicy.POOL_HIGH_THRESHOLD_KEY, model.getPoolHighThreshold());
+                            
+                            ((EntityLocal)poolEntity).emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, eventVal);
+                            
+                            if (LOG.isInfoEnabled()) {
+                                int desiredPoolSize = (int) Math.ceil(model.getCurrentPoolWorkrate() / (model.getPoolHighThreshold()/model.getPoolSize()));
+                                if (desiredPoolSize != lastEmittedDesiredPoolSize || lastEmittedPoolTemperature != TemperatureStates.HOT) {
+                                    LOG.info("{} emitted HOT (suggesting {}): {}", new Object[] {this, desiredPoolSize, eventVal});
+                                    lastEmittedDesiredPoolSize = desiredPoolSize;
+                                    lastEmittedPoolTemperature = TemperatureStates.HOT;
+                                }
+                            }
+                        }
+
+                    } catch (Exception e) {
+                        if (isRunning()) {
+                            LOG.error("Error rebalancing", e);
+                        } else {
+                            LOG.debug("Error rebalancing, but no longer running", e);
+                        }
+                    }
+                }},
+                delay,
+                TimeUnit.MILLISECONDS);
+        }
+    }
+    
+    // TODO Can get duplicate onContainerAdded events.
+    //      I presume it's because we subscribe and then iterate over the extant containers.
+    //      Solution would be for subscription to give you events for existing / current value(s).
+    //      Also current impl messes up single-threaded updates model: the setEntity is a different thread than for subscription events.
+    private void onContainerAdded(NodeType newContainer, boolean rebalanceNow) {
+        Preconditions.checkArgument(newContainer instanceof BalanceableContainer, "Added container must be a BalanceableContainer");
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording addition of container {}", this, newContainer);
+        // Low and high thresholds for the metric we're interested in are assumed to be present
+        // in the container's configuration.
+        Number lowThreshold = (Number) findConfigValue(newContainer, lowThresholdConfigKeyName);
+        Number highThreshold = (Number) findConfigValue(newContainer, highThresholdConfigKeyName);
+        if (lowThreshold == null || highThreshold == null) {
+            LOG.warn(
+                "Balanceable container '"+newContainer+"' does not define low- and high- threshold configuration keys: '"+
+                lowThresholdConfigKeyName+"' and '"+highThresholdConfigKeyName+"', skipping");
+            return;
+        }
+        
+        model.onContainerAdded(newContainer, lowThreshold.doubleValue(), highThreshold.doubleValue());
+        
+        // Note: no need to scan the container for items; they will appear via the ITEM_ADDED events.
+        // Also, must abide by any item-filters etc defined in the pool.
+        
+        if (rebalanceNow) scheduleRebalance();
+    }
+    
+    private static Object findConfigValue(Entity entity, String configKeyName) {
+        Map<ConfigKey<?>, Object> config = ((EntityInternal)entity).getAllConfig();
+        for (Entry<ConfigKey<?>, Object> entry : config.entrySet()) {
+            if (configKeyName.equals(entry.getKey().getName()))
+                return entry.getValue();
+        }
+        return null;
+    }
+    
+    // TODO Receiving duplicates of onContainerRemoved (e.g. when running LoadBalancingInmemorySoakTest)
+    private void onContainerRemoved(NodeType oldContainer, boolean rebalanceNow) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording removal of container {}", this, oldContainer);
+        model.onContainerRemoved(oldContainer);
+        if (rebalanceNow) scheduleRebalance();
+    }
+    
+    private void onItemAdded(ItemType item, NodeType parentContainer, boolean rebalanceNow) {
+        Preconditions.checkArgument(item instanceof Movable, "Added item "+item+" must implement Movable");
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording addition of item {} in container {}", new Object[] {this, item, parentContainer});
+        
+        subscribe(item, metric, eventHandler);
+        
+        // Update the model, including the current metric value (if any).
+        boolean immovable = (Boolean)elvis(item.getConfig(Movable.IMMOVABLE), false);
+        Number currentValue = item.getAttribute(metric);
+        model.onItemAdded(item, parentContainer, immovable);
+        if (currentValue != null)
+            model.onItemWorkrateUpdated(item, currentValue.doubleValue());
+        
+        if (rebalanceNow) scheduleRebalance();
+    }
+    
+    private void onItemRemoved(ItemType item, NodeType parentContainer, boolean rebalanceNow) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording removal of item {}", this, item);
+        unsubscribe(item);
+        model.onItemRemoved(item);
+        if (rebalanceNow) scheduleRebalance();
+    }
+    
+    private void onItemMoved(ItemType item, NodeType parentContainer, boolean rebalanceNow) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording moving of item {} to {}", new Object[] {this, item, parentContainer});
+        model.onItemMoved(item, parentContainer);
+        if (rebalanceNow) scheduleRebalance();
+    }
+    
+    private void onItemMetricUpdate(ItemType item, double newValue, boolean rebalanceNow) {
+        if (LOG.isTraceEnabled()) LOG.trace("{} recording metric update for item {}, new value {}", new Object[] {this, item, newValue});
+        model.onItemWorkrateUpdated(item, newValue);
+        if (rebalanceNow) scheduleRebalance();
+    }
+    
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + (groovyTruth(name) ? "("+name+")" : "");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/LocationConstraint.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/LocationConstraint.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/LocationConstraint.java
new file mode 100644
index 0000000..b58b8d2
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/LocationConstraint.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import org.apache.brooklyn.api.location.Location;
+
+/**
+ * Temporary stub to resolve dependencies in ported LoadBalancingPolicy.
+ */
+public class LocationConstraint {
+    public boolean isPermitted(Location l) { return true; } // TODO
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/Movable.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/Movable.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/Movable.java
new file mode 100644
index 0000000..68ba43d
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/Movable.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.core.util.flags.SetFromFlag;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.annotation.Effector;
+import brooklyn.entity.annotation.EffectorParam;
+import brooklyn.entity.basic.MethodEffector;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.event.basic.BasicConfigKey;
+
+
+/**
+ * Represents an item that can be migrated between balanceable containers.
+ */
+public interface Movable extends Entity {
+    
+    @SetFromFlag("immovable")
+    public static ConfigKey<Boolean> IMMOVABLE = new BasicConfigKey<Boolean>(
+        Boolean.class, "movable.item.immovable", "Indicates whether this item instance is immovable, so cannot be moved by policies", false);
+    
+    public static BasicAttributeSensor<BalanceableContainer> CONTAINER = new BasicAttributeSensor<BalanceableContainer>(
+        BalanceableContainer.class, "movable.item.container", "The container that this item is on");
+    
+    public static final MethodEffector<Void> MOVE = new MethodEffector<Void>(Movable.class, "move");
+    
+    public String getContainerId();
+    
+    @Effector(description="Moves this entity to the given container")
+    public void move(@EffectorParam(name="destination") Entity destination);
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/PolicyUtilForPool.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/PolicyUtilForPool.java b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/PolicyUtilForPool.java
new file mode 100644
index 0000000..b5c6a28
--- /dev/null
+++ b/policy/src/main/java/org/apache/brooklyn/policy/loadbalancing/PolicyUtilForPool.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.loadbalancing;
+
+import java.util.Set;
+
+/**
+ * Provides conveniences for searching for hot/cold containers in a provided pool model.
+ * Ported from Monterey v3, with irrelevant bits removed.
+ */
+public class PolicyUtilForPool<ContainerType, ItemType> {
+    
+    private final BalanceablePoolModel<ContainerType, ItemType> model;
+    
+    
+    public PolicyUtilForPool (BalanceablePoolModel<ContainerType, ItemType> model) {
+        this.model = model;
+    }
+    
+    public ContainerType findColdestContainer(Set<ContainerType> excludedContainers) {
+        return findColdestContainer(excludedContainers, null);
+    }
+    
+    /**
+     * Identifies the container with the maximum spare capacity (highThreshold - currentWorkrate),
+     * returns null if none of the model's nodes has spare capacity.
+     */
+    public ContainerType findColdestContainer(Set<ContainerType> excludedContainers, LocationConstraint locationConstraint) {
+        double maxSpareCapacity = 0;
+        ContainerType coldest = null;
+        
+        for (ContainerType c : model.getPoolContents()) {
+            if (excludedContainers.contains(c))
+                continue;
+            if (locationConstraint != null && !locationConstraint.isPermitted(model.getLocation(c)))
+                continue;
+            
+            double highThreshold = model.getHighThreshold(c);
+            double totalWorkrate = model.getTotalWorkrate(c);
+            double spareCapacity = highThreshold - totalWorkrate;
+            
+            if (highThreshold == -1 || totalWorkrate == -1) {
+                continue; // container presumably has been removed
+            }
+            if (spareCapacity > maxSpareCapacity) {
+                maxSpareCapacity = spareCapacity;
+                coldest = c;
+            }
+        }
+        return coldest;
+    }
+    
+    /**
+     * Identifies the container with the maximum overshoot (currentWorkrate - highThreshold),
+     * returns null if none of the model's  nodes has an overshoot.
+     */
+    public ContainerType findHottestContainer(Set<ContainerType> excludedContainers) {
+        double maxOvershoot = 0;
+        ContainerType hottest = null;
+        
+        for (ContainerType c : model.getPoolContents()) {
+            if (excludedContainers.contains(c))
+                continue;
+            
+            double totalWorkrate = model.getTotalWorkrate(c);
+            double highThreshold = model.getHighThreshold(c);
+            double overshoot = totalWorkrate - highThreshold;
+            
+            if (highThreshold == -1 || totalWorkrate == -1) {
+                continue; // container presumably has been removed
+            }
+            if (overshoot > maxOvershoot) {
+                maxOvershoot = overshoot;
+                hottest = c;
+            }
+        }
+        return hottest;
+    }
+    
+}
\ No newline at end of file


[08/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyTest.java b/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyTest.java
new file mode 100644
index 0000000..27441be
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/AutoScalerPolicyTest.java
@@ -0,0 +1,649 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.autoscaling;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.lang.management.MemoryUsage;
+import java.lang.management.OperatingSystemMXBean;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.policy.PolicySpec;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.apache.brooklyn.test.entity.TestCluster;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.trait.Resizable;
+import brooklyn.event.basic.BasicNotificationSensor;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Duration;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class AutoScalerPolicyTest {
+
+    private static final Logger log = LoggerFactory.getLogger(AutoScalerPolicyTest.class);
+    
+    private static long TIMEOUT_MS = 10*1000;
+    private static long SHORT_WAIT_MS = 250;
+    private static long OVERHEAD_DURATION_MS = 500;
+    private static long EARLY_RETURN_MS = 10;
+
+    private static final int MANY_TIMES_INVOCATION_COUNT = 10;
+    
+    AutoScalerPolicy policy;
+    TestCluster cluster;
+    LocallyResizableEntity resizable;
+    TestApplication app;
+    List<Integer> policyResizes = MutableList.of();
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        log.info("resetting "+getClass().getSimpleName());
+        app = TestApplication.Factory.newManagedInstanceForTests();
+        cluster = app.createAndManageChild(EntitySpec.create(TestCluster.class).configure(TestCluster.INITIAL_SIZE, 1));
+        resizable = new LocallyResizableEntity(cluster, cluster);
+        Entities.manage(resizable);
+        PolicySpec<AutoScalerPolicy> policySpec = PolicySpec.create(AutoScalerPolicy.class).configure(AutoScalerPolicy.RESIZE_OPERATOR, new ResizeOperator() {
+            @Override
+            public Integer resize(Entity entity, Integer desiredSize) {
+                log.info("resizing to "+desiredSize);
+                policyResizes.add(desiredSize);
+                return ((Resizable)entity).resize(desiredSize);
+            }
+        });
+        policy = resizable.addPolicy(policySpec);
+        policyResizes.clear();
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (policy != null) policy.destroy();
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+        cluster = null;
+        resizable = null;
+        policy = null;
+    }
+
+    public void assertSizeEventually(Integer targetSize) {
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, targetSize));
+        assertEquals(policyResizes.get(policyResizes.size()-1), targetSize);
+    }
+    
+    @Test
+    public void testShrinkColdPool() throws Exception {
+        resizable.resize(4);
+        // all metrics as per-node here
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(30d/4, 10, 20));
+        
+        // expect pool to shrink to 3 (i.e. maximum to have >= 10 per container)
+        assertSizeEventually(3);
+    }
+    
+    @Test
+    public void testShrinkColdPoolTotals() throws Exception {
+        resizable.resize(4);
+        // all metrics as totals here
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(30L, 4*10L, 4*20L));
+        
+        // expect pool to shrink to 3 (i.e. maximum to have >= 10 per container)
+        assertSizeEventually(3);
+    }
+    
+    @Test
+    public void testShrinkColdPoolRoundsUpDesiredNumberOfContainers() throws Exception {
+        resizable.resize(4);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(1L, 4*10L, 4*20L));
+        
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 1));
+    }
+
+    @Test
+    public void testGrowHotPool() throws Exception {
+        resizable.resize(2);
+        // all metrics as per-node here
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(21L, 10L, 20L));
+        
+        // expect pool to grow to 3 (i.e. minimum to have <= 20 per container)
+        assertSizeEventually(3);
+    }
+
+    @Test
+    public void testGrowHotPoolTotals() throws Exception {
+        resizable.resize(2);
+        // all metrics as totals here
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(41L, 2*10L, 2*20L));
+        
+        // expect pool to grow to 3 (i.e. minimum to have <= 20 per container)
+        assertSizeEventually(3);
+    }
+
+    @Test
+    public void testGrowShrinkRespectsResizeIterationIncrementAndResizeIterationMax() throws Exception {
+        resizable.resize(2);
+        policy.config().set(AutoScalerPolicy.RESIZE_UP_ITERATION_INCREMENT, 2);
+        policy.config().set(AutoScalerPolicy.RESIZE_UP_ITERATION_MAX, 4);
+        policy.config().set(AutoScalerPolicy.RESIZE_DOWN_ITERATION_INCREMENT, 3);
+        policy.config().set(AutoScalerPolicy.RESIZE_DOWN_ITERATION_MAX, 3);
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(42/2, 10, 20));
+        // expect pool to grow to 4 (i.e. to have <= 20 per container we need 3, but increment is 2)
+        assertSizeEventually(4);
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(200/4, 10, 20));
+        // a single hot message can only make it go to 8
+        assertSizeEventually(8);
+        assertEquals(policyResizes, MutableList.of(4, 8));
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(200/8, 10, 20));
+        assertSizeEventually(10);
+        assertEquals(policyResizes, MutableList.of(4, 8, 10));
+        
+        // now shrink
+        policyResizes.clear();
+        policy.config().set(AutoScalerPolicy.MIN_POOL_SIZE, 2);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(1, 10, 20));
+        assertSizeEventually(7);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(1, 10, 20));
+        assertSizeEventually(4);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(1, 10, 20));
+        assertSizeEventually(2);
+        assertEquals(policyResizes, MutableList.of(7, 4, 2));
+    }
+
+    @Test
+    public void testHasId() throws Exception {
+        resizable.removePolicy(policy);
+        policy = AutoScalerPolicy.builder()
+                .minPoolSize(2)
+                .build();
+        resizable.addPolicy(policy);
+        Assert.assertTrue(policy.getId()!=null);
+    }
+    
+    @Test
+    public void testNeverShrinkBelowMinimum() throws Exception {
+        resizable.removePolicy(policy);
+        policy = AutoScalerPolicy.builder()
+                .minPoolSize(2)
+                .build();
+        resizable.addPolicy(policy);
+        
+        resizable.resize(4);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 0L, 4*10L, 4*20L));
+        
+        // expect pool to shrink only to the minimum
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 2));
+    }
+    
+    @Test
+    public void testNeverGrowAboveMaximmum() throws Exception {
+        resizable.removePolicy(policy);
+        policy = AutoScalerPolicy.builder()
+                .maxPoolSize(5)
+                .build();
+        resizable.addPolicy(policy);
+        
+        resizable.resize(4);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(4, 1000000L, 4*10L, 4*20L));
+        
+        // expect pool to grow only to the maximum
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 5));
+    }
+    
+    @Test
+    public void testNeverGrowColdPool() throws Exception {
+        resizable.resize(2);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(2, 1000L, 2*10L, 2*20L));
+        
+        Thread.sleep(SHORT_WAIT_MS);
+        assertEquals(resizable.getCurrentSize(), (Integer)2);
+    }
+    
+    @Test
+    public void testNeverShrinkHotPool() throws Exception {
+        resizable.resizeSleepTime = 0;
+        resizable.resize(2);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(2, 0L, 2*10L, 2*20L));
+        
+        // if had been a POOL_COLD, would have shrunk to 3
+        Thread.sleep(SHORT_WAIT_MS);
+        assertEquals(resizable.getCurrentSize(), (Integer)2);
+    }
+    
+    @Test(groups="Integration")
+    public void testConcurrentShrinkShrink() throws Exception {
+        resizable.resizeSleepTime = 250;
+        resizable.resize(4);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 30L, 4*10L, 4*20L));
+        // would cause pool to shrink to 3
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 1L, 4*10L, 4*20L));
+        // now expect pool to shrink to 1
+        
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 1));
+    }
+    
+    @Test(groups="Integration")
+    public void testConcurrentGrowGrow() throws Exception {
+        resizable.resizeSleepTime = 250;
+        resizable.resize(2);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(2, 41L, 2*10L, 2*20L));
+        // would cause pool to grow to 3
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(2, 81L, 2*10L, 2*20L));
+        // now expect pool to grow to 5
+        
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 5));
+    }
+    
+    @Test(groups="Integration")
+    public void testConcurrentGrowShrink() throws Exception {
+        resizable.resizeSleepTime = 250;
+        resizable.resize(2);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(2, 81L, 2*10L, 2*20L));
+        // would cause pool to grow to 5
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(2, 1L, 2*10L, 2*20L));
+        // now expect pool to shrink to 1
+        
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 1));
+    }
+    
+    @Test(groups="Integration")
+    public void testConcurrentShrinkGrow() throws Exception {
+        resizable.resizeSleepTime = 250;
+        resizable.resize(4);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 1L, 4*10L, 4*20L));
+        // would cause pool to shrink to 1
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(4, 81L, 4*10L, 4*20L));
+        // now expect pool to grow to 5
+        
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 5));
+    }
+    
+    // FIXME failed in jenkins (e.g. #1035); with "lists don't have the same size expected:<3> but was:<2>"
+    // Is it just too time sensitive? But I'd have expected > 3 rather than less
+    @Test(groups="WIP")
+    public void testRepeatedQueuedResizeTakesLatestValueRatherThanIntermediateValues() throws Exception {
+        // TODO is this too time sensitive? the resize takes only 250ms so if it finishes before the next emit we'd also see size=2
+        resizable.resizeSleepTime = 500;
+        resizable.resize(4);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 30L, 4*10L, 4*20L)); // shrink to 3
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 20L, 4*10L, 4*20L)); // shrink to 2
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(4, 10L, 4*10L, 4*20L)); // shrink to 1
+        
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 1));
+        assertEquals(resizable.sizes, ImmutableList.of(4, 3, 1));
+    }
+    
+
+    @Test
+    public void testUsesResizeOperatorOverride() throws Exception {
+        resizable.removePolicy(policy);
+        
+        final AtomicInteger counter = new AtomicInteger();
+        policy = AutoScalerPolicy.builder()
+                .resizeOperator(new ResizeOperator() {
+                        @Override public Integer resize(Entity entity, Integer desiredSize) {
+                            counter.incrementAndGet();
+                            return desiredSize;
+                        }})
+                .build();
+        resizable.addPolicy(policy);
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(1, 21L, 1*10L, 1*20L)); // grow to 2
+        
+        Asserts.succeedsEventually(MutableMap.of("timeout",TIMEOUT_MS), new Runnable() {
+                public void run() {
+                    assertTrue(counter.get() >= 1, "cccounter="+counter);
+                }});
+    }
+    
+    @Test
+    public void testUsesCustomSensorOverride() throws Exception {
+        resizable.removePolicy(policy);
+        
+        @SuppressWarnings("rawtypes")
+        BasicNotificationSensor<Map> customPoolHotSensor = new BasicNotificationSensor<Map>(Map.class, "custom.hot", "");
+        @SuppressWarnings("rawtypes")
+        BasicNotificationSensor<Map> customPoolColdSensor = new BasicNotificationSensor<Map>(Map.class, "custom.cold", "");
+        @SuppressWarnings("rawtypes")
+        BasicNotificationSensor<Map> customPoolOkSensor = new BasicNotificationSensor<Map>(Map.class, "custom.ok", "");
+        policy = AutoScalerPolicy.builder()
+                .poolHotSensor(customPoolHotSensor) 
+                .poolColdSensor(customPoolColdSensor)
+                .poolOkSensor(customPoolOkSensor)
+                .build();
+        resizable.addPolicy(policy);
+        
+        resizable.emit(customPoolHotSensor, message(1, 21L, 1*10L, 1*20L)); // grow to 2
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 2));
+        
+        resizable.emit(customPoolColdSensor, message(2, 1L, 1*10L, 1*20L)); // shrink to 1
+        Asserts.succeedsEventually(ImmutableMap.of("timeout", TIMEOUT_MS), currentSizeAsserter(resizable, 1));
+    }
+    
+    @Test(groups="Integration")
+    public void testResizeUpStabilizationDelayIgnoresBlip() throws Exception {
+        long resizeUpStabilizationDelay = 1000L;
+        Duration minPeriodBetweenExecs = Duration.ZERO;
+        resizable.removePolicy(policy);
+        
+        policy = AutoScalerPolicy.builder()
+                .resizeUpStabilizationDelay(Duration.of(resizeUpStabilizationDelay, TimeUnit.MILLISECONDS)) 
+                .minPeriodBetweenExecs(minPeriodBetweenExecs)
+                .build();
+        resizable.addPolicy(policy);
+        resizable.resize(1);
+        
+        // Ignores temporary blip
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(1, 61L, 1*10L, 1*20L)); // would grow to 4
+        Thread.sleep(resizeUpStabilizationDelay-OVERHEAD_DURATION_MS);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_OK_SENSOR, message(1, 11L, 4*10L, 4*20L)); // but 1 is still adequate
+        
+        assertEquals(resizable.getCurrentSize(), (Integer)1);
+        Asserts.succeedsContinually(MutableMap.of("duration", 2000L), new Runnable() {
+                @Override public void run() {
+                    assertEquals(resizable.sizes, ImmutableList.of(1));
+                }});
+    }
+
+    // FIXME failing in jenkins occassionally - have put it in the "Acceptance" group for now
+    //
+    // Error was things like it taking a couple of seconds too long to scale-up. This is *not*
+    // just caused by a slow GC (running with -verbose:gc shows during a failure several 
+    // incremental GCs that usually don't amount to more than 0.2 of a second at most, often less).
+    // Doing a thread-dump etc immediately after the too-long delay shows no strange thread usage,
+    // and shows releng3 system load averages of numbers like 1.73, 2.87 and 1.22.
+    // 
+    // Is healthy on normal machines.
+    @Test(groups={"Integration", "Acceptance"}, invocationCount=MANY_TIMES_INVOCATION_COUNT)
+    public void testRepeatedResizeUpStabilizationDelayTakesMaxSustainedDesired() throws Throwable {
+        try {
+            testResizeUpStabilizationDelayTakesMaxSustainedDesired();
+        } catch (Throwable t) {
+            dumpThreadsEtc();
+            throw t;
+        }
+    }
+
+    @Test(groups="Integration")
+    public void testResizeUpStabilizationDelayTakesMaxSustainedDesired() throws Exception {
+        long resizeUpStabilizationDelay = 1100L;
+        Duration minPeriodBetweenExecs = Duration.ZERO;
+        resizable.removePolicy(policy);
+        
+        policy = AutoScalerPolicy.builder()
+                .resizeUpStabilizationDelay(Duration.of(resizeUpStabilizationDelay, TimeUnit.MILLISECONDS)) 
+                .minPeriodBetweenExecs(minPeriodBetweenExecs)
+                .build();
+        resizable.addPolicy(policy);
+        resizable.resize(1);
+        
+        // Will grow to only the max sustained in this time window 
+        // (i.e. to 2 within the first $resizeUpStabilizationDelay milliseconds)
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(1, 61L, 1*10L, 1*20L)); // would grow to 4
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(1, 21L, 1*10L, 1*20L)); // would grow to 2
+        Thread.sleep(resizeUpStabilizationDelay-OVERHEAD_DURATION_MS);
+        
+        long postSleepTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, message(1, 61L, 1*10L, 1*20L)); // would grow to 4
+
+        // Wait for it to reach size 2, and confirm take expected time
+        // TODO This is time sensitive, and sometimes fails in CI with size=4 if we wait for currentSize==2 (presumably GC kicking in?)
+        //      Therefore do strong assertion of currentSize==2 later, so can write out times if it goes wrong.
+        Asserts.succeedsEventually(MutableMap.of("period", 1, "timeout", TIMEOUT_MS), new Runnable() {
+            public void run() {
+                assertTrue(resizable.getCurrentSize() >= 2, "currentSize="+resizable.getCurrentSize());
+            }});
+        assertEquals(resizable.getCurrentSize(), (Integer)2, 
+                stopwatch.elapsed(TimeUnit.MILLISECONDS)+"ms after first emission; "+(stopwatch.elapsed(TimeUnit.MILLISECONDS)-postSleepTime)+"ms after last");
+        
+        long timeToResizeTo2 = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+        assertTrue(timeToResizeTo2 >= resizeUpStabilizationDelay-EARLY_RETURN_MS &&
+                timeToResizeTo2 <= resizeUpStabilizationDelay+OVERHEAD_DURATION_MS,
+                "Resizing to 2: time="+timeToResizeTo2+"; resizeUpStabilizationDelay="+resizeUpStabilizationDelay);
+
+        // Will then grow to 4 $resizeUpStabilizationDelay milliseconds after that emission
+        Asserts.succeedsEventually(MutableMap.of("period", 1, "timeout", TIMEOUT_MS), 
+                currentSizeAsserter(resizable, 4));
+        long timeToResizeTo4 = stopwatch.elapsed(TimeUnit.MILLISECONDS) - postSleepTime;
+        
+        assertTrue(timeToResizeTo4 >= resizeUpStabilizationDelay-EARLY_RETURN_MS &&
+                timeToResizeTo4 <= resizeUpStabilizationDelay+OVERHEAD_DURATION_MS,
+                "Resizing to 4: timeToResizeTo4="+timeToResizeTo4+"; timeToResizeTo2="+timeToResizeTo2+"; resizeUpStabilizationDelay="+resizeUpStabilizationDelay);
+    }
+
+    @Test(groups="Integration")
+    public void testResizeUpStabilizationDelayResizesAfterDelay() {
+        final long resizeUpStabilizationDelay = 1000L;
+        Duration minPeriodBetweenExecs = Duration.ZERO;
+        resizable.removePolicy(policy);
+        
+        policy = resizable.addPolicy(AutoScalerPolicy.builder()
+                .resizeUpStabilizationDelay(Duration.of(resizeUpStabilizationDelay, TimeUnit.MILLISECONDS)) 
+                .minPeriodBetweenExecs(minPeriodBetweenExecs)
+                .buildSpec());
+        resizable.resize(1);
+        
+        // After suitable delay, grows to desired
+        final long emitTime = System.currentTimeMillis();
+        final Map<String, Object> need4 = message(1, 61L, 1*10L, 1*20L);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, need4); // would grow to 4
+        final AtomicInteger emitCount = new AtomicInteger(0);
+        
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            public void run() {
+                if (System.currentTimeMillis() - emitTime > (2+emitCount.get())*resizeUpStabilizationDelay) {
+                    //first one may not have been received, in a registration race 
+                    resizable.emit(AutoScalerPolicy.DEFAULT_POOL_HOT_SENSOR, need4);
+                    emitCount.incrementAndGet();
+                }
+                assertEquals(resizable.getCurrentSize(), (Integer)4);
+            }});
+        
+        long resizeDelay = System.currentTimeMillis() - emitTime;
+        assertTrue(resizeDelay >= (resizeUpStabilizationDelay-EARLY_RETURN_MS), "resizeDelay="+resizeDelay);
+    }
+
+    @Test(groups="Integration")
+    public void testResizeDownStabilizationDelayIgnoresBlip() throws Exception {
+        long resizeStabilizationDelay = 1000L;
+        Duration minPeriodBetweenExecs = Duration.ZERO;
+        resizable.removePolicy(policy);
+        
+        policy = AutoScalerPolicy.builder()
+                .resizeDownStabilizationDelay(Duration.of(resizeStabilizationDelay, TimeUnit.MILLISECONDS)) 
+                .minPeriodBetweenExecs(minPeriodBetweenExecs)
+                .build();
+        resizable.addPolicy(policy);
+        resizable.resize(2);
+        
+        // Ignores temporary blip
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(2, 1L, 2*10L, 2*20L)); // would shrink to 1
+        Thread.sleep(resizeStabilizationDelay-OVERHEAD_DURATION_MS);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_OK_SENSOR, message(2, 20L, 1*10L, 1*20L)); // but 2 is still adequate
+        
+        assertEquals(resizable.getCurrentSize(), (Integer)2);
+        Asserts.succeedsContinually(MutableMap.of("duration", 2000L), new Runnable() {
+                public void run() {
+                    assertEquals(resizable.sizes, ImmutableList.of(2));
+                }});
+    }
+
+    // FIXME Acceptance -- see comment against testRepeatedResizeUpStabilizationDelayTakesMaxSustainedDesired
+    @Test(groups={"Integration", "Acceptance"}, invocationCount=MANY_TIMES_INVOCATION_COUNT)
+    public void testRepeatedResizeDownStabilizationDelayTakesMinSustainedDesired() throws Throwable {
+        try {
+            testResizeDownStabilizationDelayTakesMinSustainedDesired();
+        } catch (Throwable t) {
+            dumpThreadsEtc();
+            throw t;
+        }
+    }
+    
+    @Test(groups="Integration")
+    public void testResizeDownStabilizationDelayTakesMinSustainedDesired() throws Exception {
+        long resizeDownStabilizationDelay = 1100L;
+        Duration minPeriodBetweenExecs = Duration.ZERO;
+        policy.suspend();
+        resizable.removePolicy(policy);
+        
+        policy = AutoScalerPolicy.builder()
+                .resizeDownStabilizationDelay(Duration.of(resizeDownStabilizationDelay, TimeUnit.MILLISECONDS))
+                .minPeriodBetweenExecs(minPeriodBetweenExecs)
+                .build();
+        resizable.addPolicy(policy);
+        resizable.resize(3);
+        
+        // Will shrink to only the min sustained in this time window
+        // (i.e. to 2 within the first $resizeUpStabilizationDelay milliseconds)
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(3, 1L, 3*10L, 3*20L)); // would shrink to 1
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(3, 20L, 3*10L, 3*20L)); // would shrink to 2
+        Thread.sleep(resizeDownStabilizationDelay-OVERHEAD_DURATION_MS);
+        
+        long postSleepTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+        
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, message(3, 1L, 3*10L, 3*20L)); // would shrink to 1
+
+        // Wait for it to reach size 2, and confirm take expected time
+        // TODO This is time sensitive, and sometimes fails in CI with size=1 if we wait for currentSize==2 (presumably GC kicking in?)
+        //      Therefore do strong assertion of currentSize==2 later, so can write out times if it goes wrong.
+        Asserts.succeedsEventually(MutableMap.of("period", 1, "timeout", TIMEOUT_MS), new Runnable() {
+                public void run() {
+                    assertTrue(resizable.getCurrentSize() <= 2, "currentSize="+resizable.getCurrentSize());
+                }});
+        assertEquals(resizable.getCurrentSize(), (Integer)2, 
+                stopwatch.elapsed(TimeUnit.MILLISECONDS)+"ms after first emission; "+(stopwatch.elapsed(TimeUnit.MILLISECONDS)-postSleepTime)+"ms after last");
+        
+        long timeToResizeTo2 = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+        assertTrue(timeToResizeTo2 >= resizeDownStabilizationDelay-EARLY_RETURN_MS &&
+                timeToResizeTo2 <= resizeDownStabilizationDelay+OVERHEAD_DURATION_MS,
+                "Resizing to 2: time="+timeToResizeTo2+"; resizeDownStabilizationDelay="+resizeDownStabilizationDelay);
+
+        // Will then shrink to 1 $resizeUpStabilizationDelay milliseconds after that emission
+        Asserts.succeedsEventually(MutableMap.of("period", 1, "timeout", TIMEOUT_MS), 
+                currentSizeAsserter(resizable, 1));
+        long timeToResizeTo1 = stopwatch.elapsed(TimeUnit.MILLISECONDS) - postSleepTime;
+        
+        assertTrue(timeToResizeTo1 >= resizeDownStabilizationDelay-EARLY_RETURN_MS &&
+                timeToResizeTo1 <= resizeDownStabilizationDelay+OVERHEAD_DURATION_MS,
+                "Resizing to 1: timeToResizeTo1="+timeToResizeTo1+"; timeToResizeTo2="+timeToResizeTo2+"; resizeDownStabilizationDelay="+resizeDownStabilizationDelay);
+    }
+
+    @Test(groups="Integration")
+    public void testResizeDownStabilizationDelayResizesAfterDelay() throws Exception {
+        final long resizeDownStabilizationDelay = 1000L;
+        Duration minPeriodBetweenExecs = Duration.ZERO;
+        resizable.removePolicy(policy);
+        
+        policy = AutoScalerPolicy.builder()
+                .resizeDownStabilizationDelay(Duration.of(resizeDownStabilizationDelay, TimeUnit.MILLISECONDS))
+                .minPeriodBetweenExecs(minPeriodBetweenExecs)
+                .build();
+        resizable.addPolicy(policy);
+        resizable.resize(2);
+        
+        // After suitable delay, grows to desired
+        final long emitTime = System.currentTimeMillis();
+        final Map<String, Object> needJust1 = message(2, 1L, 2*10L, 2*20L);
+        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, needJust1); // would shrink to 1
+        final AtomicInteger emitCount = new AtomicInteger(0);
+        
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+                public void run() {
+                    if (System.currentTimeMillis() - emitTime > (2+emitCount.get())*resizeDownStabilizationDelay) {
+                        //first one may not have been received, in a registration race
+                        resizable.emit(AutoScalerPolicy.DEFAULT_POOL_COLD_SENSOR, needJust1); // would shrink to 1
+                        emitCount.incrementAndGet();
+                    }
+                    assertEquals(resizable.getCurrentSize(), (Integer)1);
+                }});
+
+        long resizeDelay = System.currentTimeMillis() - emitTime;
+        assertTrue(resizeDelay >= (resizeDownStabilizationDelay-EARLY_RETURN_MS), "resizeDelay="+resizeDelay);
+    }
+
+    Map<String, Object> message(double currentWorkrate, double lowThreshold, double highThreshold) {
+        return message(resizable.getCurrentSize(), currentWorkrate, lowThreshold, highThreshold);
+    }
+    static Map<String, Object> message(int currentSize, double currentWorkrate, double lowThreshold, double highThreshold) {
+        return ImmutableMap.<String,Object>of(
+            AutoScalerPolicy.POOL_CURRENT_SIZE_KEY, currentSize,
+            AutoScalerPolicy.POOL_CURRENT_WORKRATE_KEY, currentWorkrate,
+            AutoScalerPolicy.POOL_LOW_THRESHOLD_KEY, lowThreshold,
+            AutoScalerPolicy.POOL_HIGH_THRESHOLD_KEY, highThreshold);
+    }
+    
+    public static Runnable currentSizeAsserter(final Resizable resizable, final Integer desired) {
+        return new Runnable() {
+            public void run() {
+                assertEquals(resizable.getCurrentSize(), desired);
+            }
+        };
+    }
+    
+    public static void dumpThreadsEtc() {
+        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
+        ThreadInfo[] threads = threadMXBean.dumpAllThreads(true, true);
+        for (ThreadInfo thread : threads) {
+            System.out.println(thread.getThreadName()+" ("+thread.getThreadState()+")");
+            for (StackTraceElement stackTraceElement : thread.getStackTrace()) {
+                System.out.println("\t"+stackTraceElement);
+            }
+        }
+        
+        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
+        MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
+        MemoryUsage nonHeapMemoryUsage = memoryMXBean.getNonHeapMemoryUsage();
+        System.out.println("Memory:");
+        System.out.println("\tHeap: used="+heapMemoryUsage.getUsed()+"; max="+heapMemoryUsage.getMax()+"; init="+heapMemoryUsage.getInit()+"; committed="+heapMemoryUsage.getCommitted());
+        System.out.println("\tNon-heap: used="+nonHeapMemoryUsage.getUsed()+"; max="+nonHeapMemoryUsage.getMax()+"; init="+nonHeapMemoryUsage.getInit()+"; committed="+nonHeapMemoryUsage.getCommitted());
+
+        OperatingSystemMXBean operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean();
+        System.out.println("OS:");
+        System.out.println("\tsysLoadAvg="+operatingSystemMXBean.getSystemLoadAverage()+"; availableProcessors="+operatingSystemMXBean.getAvailableProcessors()+"; arch="+operatingSystemMXBean.getArch());
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/LocallyResizableEntity.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/LocallyResizableEntity.java b/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/LocallyResizableEntity.java
new file mode 100644
index 0000000..0b3ef9c
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/autoscaling/LocallyResizableEntity.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.autoscaling;
+
+import java.util.List;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.test.entity.TestCluster;
+
+import brooklyn.entity.basic.AbstractEntity;
+import brooklyn.entity.trait.Resizable;
+import brooklyn.entity.trait.Startable;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.Lists;
+
+/**
+ * Test class for providing a Resizable LocallyManagedEntity for policy testing
+ * It is hooked up to a TestCluster that can be used to make assertions against
+ */
+public class LocallyResizableEntity extends AbstractEntity implements Resizable {
+    List<Integer> sizes = Lists.newArrayList();
+    TestCluster cluster;
+    long resizeSleepTime = 0;
+    
+    public LocallyResizableEntity (TestCluster tc) {
+        this(null, tc);
+    }
+    @SuppressWarnings("deprecation")
+    public LocallyResizableEntity (Entity parent, TestCluster tc) {
+        super(parent);
+        this.cluster = tc;
+        setAttribute(Startable.SERVICE_UP, true);
+    }
+    
+    @Override
+    public Integer resize(Integer newSize) {
+        try {
+            Thread.sleep(resizeSleepTime);
+            sizes.add(newSize); 
+            return cluster.resize(newSize);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw Throwables.propagate(e);
+        }
+    }
+    
+    @Override
+    public Integer getCurrentSize() {
+        return cluster.getCurrentSize();
+    }
+    
+    @Override
+    public String toString() {
+        return getDisplayName();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/followthesun/AbstractFollowTheSunPolicyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/followthesun/AbstractFollowTheSunPolicyTest.java b/policy/src/test/java/org/apache/brooklyn/policy/followthesun/AbstractFollowTheSunPolicyTest.java
new file mode 100644
index 0000000..1928e3c
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/followthesun/AbstractFollowTheSunPolicyTest.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.entity.proxying.EntitySpec;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.management.ManagementContext;
+import org.apache.brooklyn.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.test.entity.TestApplication;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.DynamicGroup;
+import brooklyn.entity.basic.Entities;
+
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+
+import org.apache.brooklyn.policy.loadbalancing.BalanceableContainer;
+import org.apache.brooklyn.policy.loadbalancing.MockContainerEntity;
+import org.apache.brooklyn.policy.loadbalancing.MockItemEntity;
+import org.apache.brooklyn.policy.loadbalancing.MockItemEntityImpl;
+import org.apache.brooklyn.policy.loadbalancing.Movable;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+
+public class AbstractFollowTheSunPolicyTest {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractFollowTheSunPolicyTest.class);
+    
+    protected static final long TIMEOUT_MS = 10*1000;
+    protected static final long SHORT_WAIT_MS = 250;
+    
+    protected static final long CONTAINER_STARTUP_DELAY_MS = 100;
+    
+    protected TestApplication app;
+    protected ManagementContext managementContext;
+    protected SimulatedLocation loc1;
+    protected SimulatedLocation loc2;
+    protected FollowTheSunPool pool;
+    protected DefaultFollowTheSunModel<Entity, Movable> model;
+    protected FollowTheSunPolicy policy;
+    protected Group containerGroup;
+    protected Group itemGroup;
+    protected Random random = new Random();
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        LOG.debug("In AbstractFollowTheSunPolicyTest.setUp()");
+
+        MockItemEntityImpl.totalMoveCount.set(0);
+        MockItemEntityImpl.lastMoveTime.set(0);
+        
+        managementContext = LocalManagementContextForTests.newInstance();
+        app = ApplicationBuilder.newManagedApp(TestApplication.class, managementContext);
+        
+        loc1 = managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class).configure("name", "loc1"));
+        loc2 = managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class).configure("name", "loc2"));
+        
+        containerGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
+                .displayName("containerGroup")
+                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockContainerEntity.class)));
+        
+        itemGroup = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
+                .displayName("itemGroup")
+                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(MockItemEntity.class)));
+        model = new DefaultFollowTheSunModel<Entity, Movable>("pool-model");
+        pool = app.createAndManageChild(EntitySpec.create(FollowTheSunPool.class));
+        pool.setContents(containerGroup, itemGroup);
+        policy = new FollowTheSunPolicy(MockItemEntity.ITEM_USAGE_METRIC, model, FollowTheSunParameters.newDefault());
+        pool.addPolicy(policy);
+        app.start(ImmutableList.of(loc1, loc2));
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() {
+        if (pool != null && policy != null) pool.removePolicy(policy);
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+        MockItemEntityImpl.totalMoveCount.set(0);
+        MockItemEntityImpl.lastMoveTime.set(0);
+    }
+    
+    /**
+     * Asserts that the given container have the given expected workrates (by querying the containers directly).
+     * Accepts an accuracy of "precision" for each container's workrate.
+     */
+    protected void assertItemDistributionEventually(final Map<MockContainerEntity, ? extends Collection<MockItemEntity>> expected) {
+        try {
+            Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+                public void run() {
+                    assertItemDistribution(expected);
+                }});
+        } catch (AssertionError e) {
+            String errMsg = e.getMessage()+"; "+verboseDumpToString();
+            throw new RuntimeException(errMsg, e);
+        }
+    }
+
+    protected void assertItemDistributionContinually(final Map<MockContainerEntity, Collection<MockItemEntity>> expected) {
+        try {
+            Asserts.succeedsContinually(MutableMap.of("timeout", SHORT_WAIT_MS), new Runnable() {
+                public void run() {
+                    assertItemDistribution(expected);
+                }});
+        } catch (AssertionError e) {
+            String errMsg = e.getMessage()+"; "+verboseDumpToString();
+            throw new RuntimeException(errMsg, e);
+        }
+    }
+
+    protected void assertItemDistribution(Map<MockContainerEntity, ? extends Collection<MockItemEntity>> expected) {
+        String errMsg = verboseDumpToString();
+        for (Map.Entry<MockContainerEntity, ? extends Collection<MockItemEntity>> entry : expected.entrySet()) {
+            MockContainerEntity container = entry.getKey();
+            Collection<MockItemEntity> expectedItems = entry.getValue();
+            
+            assertEquals(ImmutableSet.copyOf(container.getBalanceableItems()), ImmutableSet.copyOf(expectedItems), errMsg);
+        }
+    }
+
+    protected String verboseDumpToString() {
+        Iterable<MockContainerEntity> containers = Iterables.filter(app.getManagementContext().getEntityManager().getEntities(), MockContainerEntity.class);
+        Iterable<MockItemEntity> items = Iterables.filter(app.getManagementContext().getEntityManager().getEntities(), MockItemEntity.class);
+        
+        Iterable<Double> containerRates = Iterables.transform(containers, new Function<MockContainerEntity, Double>() {
+            @Override public Double apply(MockContainerEntity input) {
+                return (double) input.getWorkrate();
+            }});
+        
+        Iterable<Map<Entity, Double>> containerItemUsages = Iterables.transform(containers, new Function<MockContainerEntity, Map<Entity, Double>>() {
+            @Override public Map<Entity, Double> apply(MockContainerEntity input) {
+                return input.getItemUsage();
+            }});
+        
+        Map<MockContainerEntity, Set<Movable>> itemDistributionByContainer = Maps.newLinkedHashMap();
+        for (MockContainerEntity container : containers) {
+            itemDistributionByContainer.put(container, container.getBalanceableItems());
+        }
+        
+        Map<Movable, BalanceableContainer<?>> itemDistributionByItem = Maps.newLinkedHashMap();
+        for (Movable item : items) {
+            itemDistributionByItem.put(item, item.getAttribute(Movable.CONTAINER));
+        }
+
+        String modelItemDistribution = model.itemDistributionToString();
+        
+        return "containers="+containers+"; containerRates="+containerRates
+                +"; containerItemUsages="+containerItemUsages
+                +"; itemDistributionByContainer="+itemDistributionByContainer
+                +"; itemDistributionByItem="+itemDistributionByItem
+                +"; model="+modelItemDistribution
+                +"; totalMoves="+MockItemEntityImpl.totalMoveCount
+                +"; lastMoveTime="+Time.makeDateString(MockItemEntityImpl.lastMoveTime.get());
+    }
+    
+    protected MockContainerEntity newContainer(TestApplication app, Location loc, String name) {
+        return newAsyncContainer(app, loc, name, 0);
+    }
+    
+    /**
+     * Creates a new container that will take "delay" millis to complete its start-up.
+     */
+    protected MockContainerEntity newAsyncContainer(TestApplication app, Location loc, String name, long delay) {
+        // FIXME Is this comment true?
+        // Annoyingly, can't set parent until after the threshold config has been defined.
+        MockContainerEntity container = app.createAndManageChild(EntitySpec.create(MockContainerEntity.class)
+                .displayName(name)
+                .configure(MockContainerEntity.DELAY, delay));
+        LOG.debug("Managed new container {}", container);
+        container.start(ImmutableList.of(loc));
+        return container;
+    }
+
+    protected static MockItemEntity newLockedItem(TestApplication app, MockContainerEntity container, String name) {
+        MockItemEntity item = app.createAndManageChild(EntitySpec.create(MockItemEntity.class)
+                .displayName(name)
+                .configure(MockItemEntity.IMMOVABLE, true));
+        LOG.debug("Managed new locked item {}", container);
+        if (container != null) {
+            item.move(container);
+        }
+        return item;
+    }
+    
+    protected static MockItemEntity newItem(TestApplication app, MockContainerEntity container, String name) {
+        MockItemEntity item = app.createAndManageChild(EntitySpec.create(MockItemEntity.class)
+                .displayName(name));
+        LOG.debug("Managed new item {} at {}", item, container);
+        if (container != null) {
+            item.move(container);
+        }
+        return item;
+    }
+    
+    protected static MockItemEntity newItem(TestApplication app, MockContainerEntity container, String name, Map<? extends Entity, Double> workpattern) {
+        MockItemEntity item = newItem(app, container, name);
+        if (workpattern != null) {
+            ((EntityLocal)item).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, (Map) workpattern);
+        }
+        return item;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunModelTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunModelTest.java b/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunModelTest.java
new file mode 100644
index 0000000..0edf888
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunModelTest.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+
+import org.apache.brooklyn.policy.loadbalancing.MockContainerEntity;
+import org.apache.brooklyn.policy.loadbalancing.MockContainerEntityImpl;
+import org.apache.brooklyn.policy.loadbalancing.MockItemEntity;
+import org.apache.brooklyn.policy.loadbalancing.MockItemEntityImpl;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class FollowTheSunModelTest {
+
+    private Location loc1 = new SimulatedLocation(DefaultFollowTheSunModel.newHashMap("name","loc1"));
+    private Location loc2 = new SimulatedLocation(DefaultFollowTheSunModel.newHashMap("name","loc2"));
+    private MockContainerEntity container1 = new MockContainerEntityImpl();
+    private MockContainerEntity container2 = new MockContainerEntityImpl();
+    private MockItemEntity item1 = new MockItemEntityImpl();
+    private MockItemEntity item2 = new MockItemEntityImpl();
+    private MockItemEntity item3 = new MockItemEntityImpl();
+    
+    private DefaultFollowTheSunModel<MockContainerEntity, MockItemEntity> model;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        model = new DefaultFollowTheSunModel<MockContainerEntity, MockItemEntity>("myname");
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        // noting to tear down; no management context created
+    }
+
+    @Test
+    public void testSimpleAddAndRemove() throws Exception {
+        model.onContainerAdded(container1, loc1);
+        model.onContainerAdded(container2, loc2);
+        model.onItemAdded(item1, container1, true);
+        model.onItemAdded(item2, container2, true);
+        
+        assertEquals(model.getContainerLocation(container1), loc1);
+        assertEquals(model.getContainerLocation(container2), loc2);
+        assertEquals(model.getItems(), ImmutableSet.of(item1, item2));
+        assertEquals(model.getItemLocation(item1), loc1);
+        assertEquals(model.getItemLocation(item2), loc2);
+        assertEquals(model.getItemContainer(item1), container1);
+        assertEquals(model.getItemContainer(item2), container2);
+        
+        model.onContainerRemoved(container2);
+        model.onItemRemoved(item2);
+        
+        assertEquals(model.getContainerLocation(container1), loc1);
+        assertEquals(model.getContainerLocation(container2), null);
+        assertEquals(model.getItems(), ImmutableSet.of(item1));
+        assertEquals(model.getItemLocation(item1), loc1);
+        assertEquals(model.getItemLocation(item2), null);
+        assertEquals(model.getItemContainer(item1), container1);
+        assertEquals(model.getItemContainer(item2), null);
+    }
+    
+    @Test
+    public void testItemUsageMetrics() throws Exception {
+        model.onContainerAdded(container1, loc1);
+        model.onContainerAdded(container2, loc2);
+        model.onItemAdded(item1, container1, true);
+        model.onItemAdded(item2, container2, true);
+
+        model.onItemUsageUpdated(item1, ImmutableMap.of(item2, 12d));
+        model.onItemUsageUpdated(item2, ImmutableMap.of(item1, 11d));
+        
+        assertEquals(model.getDirectSendsToItemByLocation(),
+                ImmutableMap.of(item1, ImmutableMap.of(loc2, 12d), item2, ImmutableMap.of(loc1, 11d)));
+    }
+    
+    @Test
+    public void testItemUsageReportedIfLocationSetAfterUsageUpdate() throws Exception {
+        model.onContainerAdded(container1, null);
+        model.onContainerAdded(container2, null);
+        model.onItemAdded(item1, container1, true);
+        model.onItemAdded(item2, container2, true);
+        model.onItemUsageUpdated(item1, ImmutableMap.of(item2, 12d));
+        model.onContainerLocationUpdated(container1, loc1);
+        model.onContainerLocationUpdated(container2, loc2);
+        
+        assertEquals(model.getDirectSendsToItemByLocation(),
+                ImmutableMap.of(item1, ImmutableMap.of(loc2, 12d)));
+    }
+    
+    @Test
+    public void testItemUsageMetricsSummedForActorsInSameLocation() throws Exception {
+        model.onContainerAdded(container1, loc1);
+        model.onContainerAdded(container2, loc2);
+        model.onItemAdded(item1, container1, true);
+        model.onItemAdded(item2, container2, true);
+        model.onItemAdded(item3, container2, true);
+
+        model.onItemUsageUpdated(item1, ImmutableMap.of(item2, 12d, item3, 13d));
+        
+        assertEquals(model.getDirectSendsToItemByLocation(),
+                ImmutableMap.of(item1, ImmutableMap.of(loc2, 12d+13d)));
+    }
+    
+    @Test
+    public void testItemMovedWillUpdateLocationUsage() throws Exception {
+        model.onContainerAdded(container1, loc1);
+        model.onContainerAdded(container2, loc2);
+        model.onItemAdded(item1, container1, false);
+        model.onItemAdded(item2, container2, false);
+        model.onItemUsageUpdated(item2, ImmutableMap.of(item1, 12d));
+        
+        model.onItemMoved(item1, container2);
+
+        assertEquals(model.getDirectSendsToItemByLocation(),
+                ImmutableMap.of(item2, ImmutableMap.of(loc2, 12d)));
+        assertEquals(model.getItemContainer(item1), container2);
+        assertEquals(model.getItemLocation(item1), loc2);
+    }
+    
+    @Test
+    public void testItemAddedWithNoContainer() throws Exception {
+        model.onItemAdded(item1, null, true);
+
+        assertEquals(model.getItems(), ImmutableSet.of(item1));
+        assertEquals(model.getItemContainer(item1), null);
+        assertEquals(model.getItemLocation(item1), null);
+    }
+    
+    @Test
+    public void testItemAddedBeforeContainer() throws Exception {
+        model.onItemAdded(item1, container1, true);
+        model.onContainerAdded(container1, loc1);
+
+        assertEquals(model.getItems(), ImmutableSet.of(item1));
+        assertEquals(model.getItemContainer(item1), container1);
+        assertEquals(model.getItemLocation(item1), loc1);
+    }
+    
+    @Test
+    public void testItemMovedBeforeContainerAdded() throws Exception {
+        model.onContainerAdded(container1, loc1);
+        model.onItemAdded(item1, container1, true);
+        model.onItemMoved(item1, container2);
+        model.onContainerAdded(container2, loc2);
+
+        assertEquals(model.getItems(), ImmutableSet.of(item1));
+        assertEquals(model.getItemContainer(item1), container2);
+        assertEquals(model.getItemLocation(item1), loc2);
+    }
+    
+    @Test
+    public void testItemAddedAnswersMovability() throws Exception {
+        model.onItemAdded(item1, container1, false);
+        model.onItemAdded(item2, container1, true);
+        assertTrue(model.isItemMoveable(item1));
+        assertFalse(model.isItemMoveable(item2));
+    }
+    
+    @Test
+    public void testWorkrateUpdateAfterItemRemovalIsNotRecorded() throws Exception {
+        model.onContainerAdded(container1, loc1);
+        model.onItemAdded(item1, container1, true);
+        model.onItemAdded(item2, container1, true);
+        model.onItemRemoved(item1);
+        model.onItemUsageUpdated(item1, ImmutableMap.of(item2, 123d));
+        
+        assertFalse(model.getDirectSendsToItemByLocation().containsKey(item1));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicySoakTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicySoakTest.java b/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicySoakTest.java
new file mode 100644
index 0000000..178a9ae
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicySoakTest.java
@@ -0,0 +1,274 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.location.Location;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+
+import org.apache.brooklyn.policy.loadbalancing.BalanceableContainer;
+import org.apache.brooklyn.policy.loadbalancing.MockContainerEntity;
+import org.apache.brooklyn.policy.loadbalancing.MockItemEntity;
+import org.apache.brooklyn.policy.loadbalancing.MockItemEntityImpl;
+import org.apache.brooklyn.policy.loadbalancing.Movable;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+
+public class FollowTheSunPolicySoakTest extends AbstractFollowTheSunPolicyTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunPolicySoakTest.class);
+    
+    private static final long TIMEOUT_MS = 10*1000;
+    
+    @Test
+    public void testFollowTheSunQuickTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 1;
+        config.numLocations=3;
+        config.numContainersPerLocation = 5;
+        config.numLockedItemsPerLocation = 2;
+        config.numMovableItems = 10;
+    
+        runFollowTheSunSoakTest(config);
+    }
+    
+    @Test
+    public void testLoadBalancingManyItemsQuickTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 1;
+        config.numLocations=2;
+        config.numContainersPerLocation = 3;
+        config.numLockedItemsPerLocation = 2;
+        config.numMovableItems = 10;
+        config.numContainerStopsPerCycle = 1;
+        config.numItemStopsPerCycle = 1;
+    
+        runFollowTheSunSoakTest(config);
+    }
+    
+    @Test(groups={"Integration"}) // takes ~2s
+    public void testLoadBalancingManyItemsNotTooLongTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 1;
+        config.numLocations=3;
+        config.numContainersPerLocation = 5;
+        config.numLockedItemsPerLocation = 2;
+        config.numMovableItems = 500;
+        config.numContainerStopsPerCycle = 1;
+        config.numItemStopsPerCycle = 1;
+    
+        runFollowTheSunSoakTest(config);
+    }
+    
+    @Test(groups={"Integration","Acceptance"}) // integration group, because it's slow to run many cycles
+    public void testLoadBalancingSoakTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 100;
+        config.numLocations=3;
+        config.numContainersPerLocation = 5;
+        config.numLockedItemsPerLocation = 2;
+        config.numMovableItems = 10;
+    
+        runFollowTheSunSoakTest(config);
+    }
+
+    @Test(groups={"Integration","Acceptance"}) // integration group, because it's slow to run many cycles
+    public void testLoadBalancingManyItemsSoakTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 100;
+        config.numLocations=3;
+        config.numContainersPerLocation = 5;
+        config.numLockedItemsPerLocation = 2;
+        config.numMovableItems = 100;
+        config.numContainerStopsPerCycle = 3;
+        config.numItemStopsPerCycle = 10;
+        
+        runFollowTheSunSoakTest(config);
+    }
+
+    @Test(groups={"Integration","Acceptance"}) // integration group, because it's slow to run many cycles
+    public void testLoadBalancingManyManyItemsTest() {
+        RunConfig config = new RunConfig();
+        config.numCycles = 1;
+        config.numLocations=10;
+        config.numContainersPerLocation = 5;
+        config.numLockedItemsPerLocation = 100;
+        config.numMovableItems = 1000;
+        config.numContainerStopsPerCycle = 0;
+        config.numItemStopsPerCycle = 0;
+        config.timeout_ms = 30*1000;
+        config.verbose = false;
+        
+        runFollowTheSunSoakTest(config);
+    }
+    
+    private void runFollowTheSunSoakTest(RunConfig config) {
+        int numCycles = config.numCycles;
+        int numLocations = config.numLocations;
+        int numContainersPerLocation = config.numContainersPerLocation;
+        int numLockedItemsPerLocation = config.numLockedItemsPerLocation;
+        int numMovableItems = config.numMovableItems;
+        
+        int numContainerStopsPerCycle = config.numContainerStopsPerCycle;
+        int numItemStopsPerCycle = config.numItemStopsPerCycle;
+        long timeout_ms = config.timeout_ms;
+        final boolean verbose = config.verbose;
+        
+        MockItemEntityImpl.totalMoveCount.set(0);
+        
+        List<Location> locations = new ArrayList<Location>();
+        Multimap<Location,MockContainerEntity> containers = HashMultimap.<Location,MockContainerEntity>create();
+        Multimap<Location,MockItemEntity> lockedItems = HashMultimap.<Location,MockItemEntity>create();
+        final List<MockItemEntity> movableItems = new ArrayList<MockItemEntity>();
+        
+        for (int i = 1; i <= numLocations; i++) {
+            String locName = "loc"+i;
+            Location loc = new SimulatedLocation(MutableMap.of("name",locName));
+            locations.add(loc);
+            
+            for (int j = 1; j <= numContainersPerLocation; j++) {
+                MockContainerEntity container = newContainer(app, loc, "container-"+locName+"-"+j);
+                containers.put(loc, container);
+            }
+            for (int j = 1; j <= numLockedItemsPerLocation; j++) {
+                MockContainerEntity container = Iterables.get(containers.get(loc), j%numContainersPerLocation);
+                MockItemEntity item = newLockedItem(app, container, "item-locked-"+locName+"-"+j);
+                lockedItems.put(loc, item);
+            }
+        }
+        
+        for (int i = 1; i <= numMovableItems; i++) {
+            MockContainerEntity container = Iterables.get(containers.values(), i%containers.size());
+            MockItemEntity item = newItem(app, container, "item-movable"+i);
+            movableItems.add(item);
+        }
+
+        for (int i = 1; i <= numCycles; i++) {
+            LOG.info("{}: cycle {}", FollowTheSunPolicySoakTest.class.getSimpleName(), i);
+            
+            // Stop movable items, and start others
+            for (int j = 1; j <= numItemStopsPerCycle; j++) {
+                int itemIndex = random.nextInt(numMovableItems);
+                MockItemEntity itemToStop = movableItems.get(itemIndex);
+                itemToStop.stop();
+                LOG.debug("Unmanaging item {}", itemToStop);
+                Entities.unmanage(itemToStop);
+                movableItems.set(itemIndex, newItem(app, Iterables.get(containers.values(), 0), "item-movable"+itemIndex));
+            }
+
+            // Choose a location to be busiest
+            int locIndex = random.nextInt(numLocations);
+            final Location busiestLocation = locations.get(locIndex);
+            
+            // Repartition the load across the items
+            for (int j = 0; j < numMovableItems; j++) {
+                MockItemEntity item = movableItems.get(j);
+                Map<Entity, Double> workrates = Maps.newLinkedHashMap();
+                
+                for (Map.Entry<Location,MockItemEntity> entry : lockedItems.entries()) {
+                    Location location = entry.getKey();
+                    MockItemEntity source = entry.getValue();
+                    double baseWorkrate = (location == busiestLocation ? 1000 : 100);
+                    double jitter = 10;
+                    double jitteredWorkrate = Math.max(0, baseWorkrate + (random.nextDouble()*jitter*2 - jitter));
+                    workrates.put(source, jitteredWorkrate);
+                }
+                ((EntityLocal)item).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, workrates);
+            }
+
+            // Stop containers, and start others
+            // This offloads the "immovable" items to other containers in the same location!
+            for (int j = 1; j <= numContainerStopsPerCycle; j++) {
+                int containerIndex = random.nextInt(containers.size());
+                MockContainerEntity containerToStop = Iterables.get(containers.values(), containerIndex);
+                Location location = Iterables.get(containerToStop.getLocations(), 0);
+                MockContainerEntity otherContainerInLocation = Iterables.find(containers.get(location), Predicates.not(Predicates.equalTo(containerToStop)), null);
+                containerToStop.offloadAndStop(otherContainerInLocation);
+                LOG.debug("Unmanaging container {}", containerToStop);
+                Entities.unmanage(containerToStop);
+                containers.remove(location, containerToStop);
+                
+                MockContainerEntity containerToAdd = newContainer(app, location, "container-"+location.getDisplayName()+"-new."+i+"."+j);
+                containers.put(location, containerToAdd);
+            }
+
+            // Assert that the items all end up in the location with maximum load-generation
+            Asserts.succeedsEventually(MutableMap.of("timeout", timeout_ms), new Runnable() {
+                public void run() {
+                    Iterable<Location> itemLocs = Iterables.transform(movableItems, new Function<MockItemEntity, Location>() {
+                        public Location apply(MockItemEntity input) {
+                            BalanceableContainer<?> container = input.getAttribute(Movable.CONTAINER);
+                            Collection<Location> locs = (container != null) ? container.getLocations(): null;
+                            return (locs != null && locs.size() > 0) ? Iterables.get(locs, 0) : null;
+                        }});
+                    
+                    Iterable<String> itemLocNames = Iterables.transform(itemLocs, new Function<Location, String>() {
+                        public String apply(Location input) {
+                            return (input != null) ? input.getDisplayName() : null;
+                        }});
+                    String errMsg;
+                    if (verbose) {
+                        errMsg = verboseDumpToString()+"; itemLocs="+itemLocNames;
+                    } else {
+                        Set<String> locNamesInUse = Sets.newLinkedHashSet(itemLocNames);
+                        errMsg = "locsInUse="+locNamesInUse+"; totalMoves="+MockItemEntityImpl.totalMoveCount;
+                    }
+                    
+                    assertEquals(ImmutableList.copyOf(itemLocs), Collections.nCopies(movableItems.size(), busiestLocation), errMsg);
+                }});
+        }
+    }
+    
+    static class RunConfig {
+        int numCycles = 1;
+        int numLocations = 3;
+        int numContainersPerLocation = 5;
+        int numLockedItemsPerLocation = 5;
+        int numMovableItems = 5;
+        int numContainerStopsPerCycle = 0;
+        int numItemStopsPerCycle = 0;
+        long timeout_ms = TIMEOUT_MS;
+        boolean verbose = true;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicyTest.java
----------------------------------------------------------------------
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicyTest.java b/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicyTest.java
new file mode 100644
index 0000000..872efab
--- /dev/null
+++ b/policy/src/test/java/org/apache/brooklyn/policy/followthesun/FollowTheSunPolicyTest.java
@@ -0,0 +1,307 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.policy.followthesun;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.basic.EntityLocal;
+import org.apache.brooklyn.api.event.SensorEvent;
+import org.apache.brooklyn.api.event.SensorEventListener;
+import org.apache.brooklyn.api.location.Location;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+
+import org.apache.brooklyn.location.basic.SimulatedLocation;
+
+import org.apache.brooklyn.policy.loadbalancing.MockContainerEntity;
+import org.apache.brooklyn.policy.loadbalancing.MockItemEntity;
+import org.apache.brooklyn.policy.loadbalancing.Movable;
+import brooklyn.test.Asserts;
+import brooklyn.util.collections.MutableMap;
+
+import com.google.common.base.Function;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+
+public class FollowTheSunPolicyTest extends AbstractFollowTheSunPolicyTest {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunPolicyTest.class);
+
+    @Test
+    public void testPolicyUpdatesModel() {
+        final MockContainerEntity containerA = newContainer(app, loc1, "A");
+        final MockItemEntity item1 = newItem(app, containerA, "1");
+        final MockItemEntity item2 = newItem(app, containerA, "2");
+        ((EntityLocal)item2).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item1, 11d));
+        
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            @Override public void run() {
+                assertEquals(ImmutableSet.of(item1, item2), model.getItems());
+                assertEquals(model.getItemContainer(item1), containerA);
+                assertEquals(model.getItemLocation(item1), loc1);
+                assertEquals(model.getContainerLocation(containerA), loc1);
+                assertEquals(model.getDirectSendsToItemByLocation(), ImmutableMap.of(item2, ImmutableMap.of(loc1, 11d)));
+            }});
+    }
+    
+    @Test
+    public void testPolicyAcceptsLocationFinder() {
+        pool.removePolicy(policy);
+        
+        Function<Entity, Location> customLocationFinder = new Function<Entity, Location>() {
+            @Override public Location apply(Entity input) {
+                return new SimulatedLocation(MutableMap.of("name", "custom location for "+input));
+            }};
+        
+        FollowTheSunPolicy customPolicy = new FollowTheSunPolicy(
+                MutableMap.of("minPeriodBetweenExecs", 0, "locationFinder", customLocationFinder), 
+                MockItemEntity.ITEM_USAGE_METRIC, 
+                model, 
+                FollowTheSunParameters.newDefault());
+        
+        pool.addPolicy(customPolicy);
+        
+        final MockContainerEntity containerA = newContainer(app, loc1, "A");
+        
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            @Override public void run() {
+                assertEquals(model.getContainerLocation(containerA).getDisplayName(), "custom location for "+containerA);
+            }});
+    }
+    
+    @Test
+    public void testNoopBalancing() throws Exception {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, loc1, "A");
+        MockContainerEntity containerB = newContainer(app, loc2, "B");
+        MockItemEntity item1 = newItem(app, containerA, "1", Collections.<Entity, Double>emptyMap());
+        MockItemEntity item2 = newItem(app, containerB, "2", Collections.<Entity, Double>emptyMap());
+        
+        Thread.sleep(SHORT_WAIT_MS);
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
+    }
+    
+    @Test
+    public void testMovesItemToFollowDemand() throws Exception {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, loc1, "A");
+        MockContainerEntity containerB = newContainer(app, loc2, "B");
+        MockItemEntity item1 = newItem(app, containerA, "1");
+        MockItemEntity item2 = newItem(app, containerB, "2");
+
+        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
+        
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.<MockItemEntity>of(), containerB, ImmutableList.of(item1, item2)));
+    }
+    
+    @Test
+    public void testNoopIfDemandIsTiny() throws Exception {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, loc1, "A");
+        MockContainerEntity containerB = newContainer(app, loc2, "B");
+        MockItemEntity item1 = newItem(app, containerA, "1");
+        MockItemEntity item2 = newItem(app, containerB, "2");
+
+        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 0.1d));
+        
+        Thread.sleep(SHORT_WAIT_MS);
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
+    }
+    
+    @Test
+    public void testNoopIfDemandIsSimilarToCurrentLocation() throws Exception {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, loc1, "A");
+        MockContainerEntity containerB = newContainer(app, loc2, "B");
+        MockItemEntity item1 = newItem(app, containerA, "1");
+        MockItemEntity item2 = newItem(app, containerA, "2");
+        MockItemEntity item3 = newItem(app, containerB, "3");
+        
+        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d, item3, 100.1d));
+        
+        Thread.sleep(SHORT_WAIT_MS);
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2), containerB, ImmutableList.of(item3)));
+    }
+    
+    @Test
+    public void testMoveDecisionIgnoresDemandFromItself() throws Exception {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, loc1, "A");
+        MockContainerEntity containerB = newContainer(app, loc2, "B");
+        MockItemEntity item1 = newItem(app, containerA, "1");
+        MockItemEntity item2 = newItem(app, containerB, "2");
+        
+        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item1, 100d));
+        ((EntityLocal)item2).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
+        
+        Thread.sleep(SHORT_WAIT_MS);
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
+    }
+    
+    @Test
+    public void testItemRemovedCausesRecalculationOfOptimalLocation() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, loc1, "A");
+        MockContainerEntity containerB = newContainer(app, loc2, "B");
+        MockItemEntity item1 = newItem(app, containerA, "1");
+        MockItemEntity item2 = newItem(app, containerA, "2");
+        MockItemEntity item3 = newItem(app, containerB, "3");
+        
+        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d, item3, 1000d));
+        
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item2), containerB, ImmutableList.of(item1, item3)));
+        
+        item3.stop();
+        Entities.unmanage(item3);
+        
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2), containerB, ImmutableList.<MockItemEntity>of()));
+    }
+    
+    @Test
+    public void testItemMovedCausesRecalculationOfOptimalLocationForOtherItems() {
+        // Set-up containers and items.
+        MockContainerEntity containerA = newContainer(app, loc1, "A");
+        MockContainerEntity containerB = newContainer(app, loc2, "B");
+        MockItemEntity item1 = newItem(app, containerA, "1");
+        MockItemEntity item2 = newItem(app, containerA, "2");
+        MockItemEntity item3 = newItem(app, containerB, "3");
+        
+        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d, item3, 1000d));
+        
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item2), containerB, ImmutableList.of(item1, item3)));
+        
+        item3.move(containerA);
+        
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2, item3), containerB, ImmutableList.<MockItemEntity>of()));
+    }
+    
+    @Test
+    public void testImmovableItemIsNotMoved() {
+        MockContainerEntity containerA = newContainer(app, loc1, "A");
+        MockContainerEntity containerB = newContainer(app, loc2, "B");
+        MockItemEntity item1 = newLockedItem(app, containerA, "1");
+        MockItemEntity item2 = newItem(app, containerB, "2");
+        
+        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
+        
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
+    }
+    
+    @Test
+    public void testImmovableItemContributesTowardsLoad() {
+        MockContainerEntity containerA = newContainer(app, loc1, "A");
+        MockContainerEntity containerB = newContainer(app, loc2, "B");
+        MockItemEntity item1 = newLockedItem(app, containerA, "1");
+        MockItemEntity item2 = newItem(app, containerA, "2");
+        
+        ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item1, 100d));
+        
+        assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2), containerB, ImmutableList.<MockItemEntity>of()));
+    }
+
+    // Marked as "Acceptance" due to time-sensitive nature :-(
+    @Test(groups={"Integration", "Acceptance"}, invocationCount=20)
+    public void testRepeatedRespectsMinPeriodBetweenExecs() throws Exception {
+        testRespectsMinPeriodBetweenExecs();
+    }
+    
+    @Test(groups="Integration")
+    public void testRespectsMinPeriodBetweenExecs() throws Exception {
+        // Failed in jenkins several times, e.g. with event times [2647, 2655] and [1387, 2001].
+        // Aled's guess is that there was a delay notifying the listener the first time
+        // (which happens async), causing the listener to be notified in rapid 
+        // succession. The policy execs probably did happen with a 1000ms separation.
+        // 
+        // Therefore try up to three times to see if we get the desired separation. If the 
+        // minPeriodBetweenExecs wasn't being respected, we'd expect the default 100ms; this 
+        // test would therefore hardly ever pass.
+        final int MAX_ATTEMPTS = 3;
+
+        final long minPeriodBetweenExecs = 1000;
+        final long timePrecision = 250;
+        
+        pool.removePolicy(policy);
+        
+        MockContainerEntity containerA = newContainer(app, loc1, "A");
+        MockContainerEntity containerB = newContainer(app, loc2, "B");
+        MockItemEntity item1 = newItem(app, containerA, "1");
+        MockItemEntity item2 = newItem(app, containerB, "2");
+        MockItemEntity item3 = newItem(app, containerA, "3");
+        
+        FollowTheSunPolicy customPolicy = new FollowTheSunPolicy(
+            MutableMap.of("minPeriodBetweenExecs", minPeriodBetweenExecs),
+            MockItemEntity.ITEM_USAGE_METRIC,
+            model,
+            FollowTheSunParameters.newDefault());
+    
+        pool.addPolicy(customPolicy);
+        
+        // Record times that things are moved, by lisening to the container sensor being set
+        final Stopwatch stopwatch = Stopwatch.createStarted();
+        
+        final List<Long> eventTimes = Lists.newCopyOnWriteArrayList();
+        final Semaphore semaphore = new Semaphore(0);
+        
+        app.subscribe(item1, Movable.CONTAINER, new SensorEventListener<Entity>() {
+            @Override public void onEvent(SensorEvent<Entity> event) {
+                long eventTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
+                LOG.info("Received {} at {}", event, eventTime);
+                eventTimes.add(eventTime);
+                semaphore.release();
+            }});
+
+        String errmsg = "";
+        for (int i = 0; i < MAX_ATTEMPTS; i++) {
+            // Set the workrate, causing the policy to move item1 to item2's location, and wait for it to happen
+            ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
+            assertTrue(semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertEquals(item1.getAttribute(Movable.CONTAINER), containerB);
+            
+            // now cause item1 to be moved to item3's location, and wait for it to happen
+            ((EntityLocal)item1).setAttribute(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item3, 100d));
+            assertTrue(semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            assertEquals(item1.getAttribute(Movable.CONTAINER), containerA);
+            
+            LOG.info("testRepeatedRespectsMinPeriodBetweenExecs event times: "+eventTimes);
+            assertEquals(eventTimes.size(), 2);
+            if (eventTimes.get(1) - eventTimes.get(0) > (minPeriodBetweenExecs-timePrecision)) {
+                return; // success
+            } else {
+                errmsg += eventTimes;
+                eventTimes.clear();
+            }
+        }
+        
+        fail("Event times never had sufficient gap: "+errmsg);
+    }
+}


[21/24] incubator-brooklyn git commit: make initialize database optional

Posted by he...@apache.org.
make initialize database optional


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

Branch: refs/heads/master
Commit: 3fff64171269356387f5e2a45a2eb392a3e6f356
Parents: 8cba4d3
Author: Robert Moss <ro...@gmail.com>
Authored: Tue Aug 18 11:37:57 2015 +0100
Committer: Robert Moss <ro...@gmail.com>
Committed: Tue Aug 18 11:37:57 2015 +0100

----------------------------------------------------------------------
 .../database/postgresql/PostgreSqlNode.java     | 13 +++-
 .../postgresql/PostgreSqlSshDriver.java         | 75 ++++++++++++--------
 2 files changed, 55 insertions(+), 33 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3fff6417/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
----------------------------------------------------------------------
diff --git a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
index b90d19e..eccb870 100644
--- a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
+++ b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlNode.java
@@ -82,10 +82,15 @@ public interface PostgreSqlNode extends SoftwareProcess, HasShortName, Datastore
     ConfigKey<Long> POLL_PERIOD = ConfigKeys.newLongConfigKey(
             "postgresql.sensorpoll", "Poll period (in milliseconds)", 1000L);
     
+    @SetFromFlag("initializeDB")
+    ConfigKey<Boolean> INITIALIZE_DB = ConfigKeys.newBooleanConfigKey(
+            "postgresql.initialize", "If true, PostgreSQL will create a new user and database", false);
+
     @SetFromFlag("username")
     BasicAttributeSensorAndConfigKey<String> USERNAME = new BasicAttributeSensorAndConfigKey<>(
-            String.class, "postgresql.username", "Username of the database user",
-            "postgresuser");
+            String.class, "postgresql.username", "Username of the database user");
+    
+    String DEFAULT_USERNAME = "postgresqluser";
     
     @SetFromFlag("password")
     BasicAttributeSensorAndConfigKey<String> PASSWORD = new BasicAttributeSensorAndConfigKey<>(
@@ -94,7 +99,9 @@ public interface PostgreSqlNode extends SoftwareProcess, HasShortName, Datastore
 
     @SetFromFlag("database")
     BasicAttributeSensorAndConfigKey<String> DATABASE = new BasicAttributeSensorAndConfigKey<>(
-            String.class, "postgresql.database", "Database to be used", "db");
+            String.class, "postgresql.database", "Database to be used");
+    
+    String DEFAULT_DB_NAME = "db";
 
     Effector<String> EXECUTE_SCRIPT = Effectors.effector(DatastoreMixins.EXECUTE_SCRIPT)
             .description("Executes the given script contents using psql")

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3fff6417/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
----------------------------------------------------------------------
diff --git a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
index b7b5722..f8133f8 100644
--- a/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
+++ b/software/database/src/main/java/brooklyn/entity/database/postgresql/PostgreSqlSshDriver.java
@@ -38,6 +38,12 @@ import java.io.InputStream;
 
 import javax.annotation.Nullable;
 
+import org.apache.brooklyn.api.location.OsDetails;
+import org.apache.brooklyn.core.util.task.DynamicTasks;
+import org.apache.brooklyn.core.util.task.ssh.SshTasks;
+import org.apache.brooklyn.core.util.task.ssh.SshTasks.OnFailingTask;
+import org.apache.brooklyn.core.util.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,15 +52,7 @@ import brooklyn.entity.basic.Attributes;
 import brooklyn.entity.basic.SoftwareProcess;
 import brooklyn.entity.database.DatastoreMixins;
 import brooklyn.entity.software.SshEffectorTasks;
-
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.location.OsDetails;
-import org.apache.brooklyn.core.util.task.DynamicTasks;
-import org.apache.brooklyn.core.util.task.ssh.SshTasks;
-import org.apache.brooklyn.core.util.task.ssh.SshTasks.OnFailingTask;
-import org.apache.brooklyn.core.util.task.system.ProcessTaskWrapper;
-import org.apache.brooklyn.location.basic.SshMachineLocation;
-
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
 import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.exceptions.Exceptions;
@@ -296,15 +294,32 @@ public class PostgreSqlSshDriver extends AbstractSoftwareProcessSshDriver implem
 
         // Wait for commands to complete before running the creation script
         DynamicTasks.waitForLast();
+        if(entity.getConfig(PostgreSqlNode.INITIALIZE_DB)){
+            initializeNewDatabase();
+        }
+        // Capture log file contents if there is an error configuring the database
+        try {
+            executeDatabaseCreationScript();
+        } catch (RuntimeException r) {
+            logTailOfPostgresLog();
+            throw Exceptions.propagate(r);
+        }
+
+        // Try establishing an external connection. If you get a "Connection refused...accepting TCP/IP connections
+        // on port 5432?" error then the port is probably closed. Check that the firewall allows external TCP/IP
+        // connections (netstat -nap). You can open a port with lokkit or by configuring the iptables.
+    }
+
+    private void initializeNewDatabase() {
         String createUserCommand = String.format(
                 "\"CREATE USER %s WITH PASSWORD '%s'; \"",
-                StringEscapes.escapeSql(entity.getConfig(PostgreSqlNode.USERNAME)), 
+                StringEscapes.escapeSql(getUsername()), 
                 StringEscapes.escapeSql(getUserPassword())
         );
         String createDatabaseCommand = String.format(
                 "\"CREATE DATABASE %s OWNER %s\"",
-                StringEscapes.escapeSql(entity.getConfig(PostgreSqlNode.DATABASE)),
-                StringEscapes.escapeSql(entity.getConfig(PostgreSqlNode.USERNAME)));
+                StringEscapes.escapeSql(getDatabaseName()),
+                StringEscapes.escapeSql(getUsername()));
         newScript("initializing user and database")
         .body.append(
                 "cd " + getInstallDir(),
@@ -315,28 +330,28 @@ public class PostgreSqlSshDriver extends AbstractSoftwareProcessSshDriver implem
                                 " --command="+ createDatabaseCommand),
                 callPgctl("stop", true))
                 .failOnNonZeroResultCode().execute();
-        // Capture log file contents if there is an error configuring the database
-        try {
-            executeDatabaseCreationScript();
-        } catch (RuntimeException r) {
-            logTailOfPostgresLog();
-            throw Exceptions.propagate(r);
+    }
+    
+    private String getConfigOrDefault(BasicAttributeSensorAndConfigKey<String> key, String def) {
+        String config = entity.getConfig(key);
+        if(Strings.isEmpty(config)) {
+            config = def;
+            log.debug(entity + " has no config specified for " + key + "; using default `" + def + "`");
+            entity.setAttribute(key, config);
         }
-
-        // Try establishing an external connection. If you get a "Connection refused...accepting TCP/IP connections
-        // on port 5432?" error then the port is probably closed. Check that the firewall allows external TCP/IP
-        // connections (netstat -nap). You can open a port with lokkit or by configuring the iptables.
+        return config;
+    }
+    
+    protected String getDatabaseName() {
+        return getConfigOrDefault(PostgreSqlNode.DATABASE, PostgreSqlNode.DEFAULT_DB_NAME);
+    }
+    
+    protected String getUsername(){
+        return getConfigOrDefault(PostgreSqlNode.USERNAME, PostgreSqlNode.DEFAULT_USERNAME);
     }
     
     protected String getUserPassword() {
-        String password = entity.getConfig(PostgreSqlNode.PASSWORD);
-        if (Strings.isEmpty(password)) {
-            log.debug(entity + " has no password specified for " + PostgreSqlNode.PASSWORD + "; using a random string");
-            password = brooklyn.util.text.Strings.makeRandomId(8);
-            entity.setAttribute(PostgreSqlNode.PASSWORD, password);
-            entity.config().set(PostgreSqlNode.PASSWORD, password);
-        }
-        return password;
+        return getConfigOrDefault(PostgreSqlNode.PASSWORD, Strings.makeRandomId(8));
     }
 
     protected void executeDatabaseCreationScript() {


[20/24] incubator-brooklyn git commit: [BROOKLYN-162] Renaming package policy

Posted by he...@apache.org.
[BROOKLYN-162] Renaming package policy


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

Branch: refs/heads/master
Commit: d30ff597bab1c751d8767b95e547ab995d7709e2
Parents: 5dfe944
Author: Ivana Yovcheva <iv...@gmail.com>
Authored: Mon Aug 17 14:49:58 2015 +0300
Committer: Ivana Yovcheva <iv...@gmail.com>
Committed: Tue Aug 18 12:30:48 2015 +0300

----------------------------------------------------------------------
 .../yaml/example_yaml/appserver-w-policy.yaml   |    2 +-
 .../vanilla-bash-netcat-restarter.yaml          |    4 +-
 .../vanilla-bash-netcat-w-client.yaml           |    4 +-
 ...followthesun.DefaultFollowTheSunModel$1.html |    6 +-
 .../brooklyn/demo/CumulusRDFApplication.java    |    6 +-
 .../demo/HighAvailabilityCassandraCluster.java  |    6 +-
 .../brooklyn/demo/ResilientMongoDbApp.java      |    6 +-
 .../brooklyn/demo/RiakClusterExample.java       |    4 +-
 .../brooklyn/demo/WideAreaCassandraCluster.java |    6 +-
 .../brooklyn/demo/ha-cassandra-cluster.yaml     |    6 +-
 .../demo/wide-area-cassandra-cluster.yaml       |    6 +-
 .../demo/WebClusterDatabaseExample.java         |    2 +-
 .../demo/WebClusterDatabaseExampleApp.java      |    3 +-
 .../demo/WebClusterDatabaseExampleGroovy.groovy |    5 +-
 .../apache/brooklyn/demo/WebClusterExample.java |    2 +-
 ...lusterDatabaseExampleAppIntegrationTest.java |    2 +-
 .../policy/autoscaling/AutoScalerPolicy.java    | 1090 -----------------
 .../autoscaling/MaxPoolSizeReachedEvent.java    |  103 --
 .../policy/autoscaling/ResizeOperator.java      |   31 -
 .../policy/autoscaling/SizeHistory.java         |  166 ---
 .../followthesun/DefaultFollowTheSunModel.java  |  328 ------
 .../policy/followthesun/FollowTheSunModel.java  |   56 -
 .../followthesun/FollowTheSunParameters.java    |   95 --
 .../policy/followthesun/FollowTheSunPolicy.java |  280 -----
 .../policy/followthesun/FollowTheSunPool.java   |   75 --
 .../followthesun/FollowTheSunPoolImpl.java      |  178 ---
 .../followthesun/FollowTheSunStrategy.java      |  161 ---
 .../policy/followthesun/WeightedObject.java     |   71 --
 .../policy/ha/AbstractFailureDetector.java      |  361 ------
 .../policy/ha/ConditionalSuspendPolicy.java     |  103 --
 .../policy/ha/ConnectionFailureDetector.java    |  126 --
 .../main/java/brooklyn/policy/ha/HASensors.java |   62 -
 .../policy/ha/ServiceFailureDetector.java       |  340 ------
 .../brooklyn/policy/ha/ServiceReplacer.java     |  214 ----
 .../brooklyn/policy/ha/ServiceRestarter.java    |  163 ---
 .../policy/ha/SshMachineFailureDetector.java    |  102 --
 .../loadbalancing/BalanceableContainer.java     |   51 -
 .../loadbalancing/BalanceablePoolModel.java     |   64 -
 .../loadbalancing/BalanceableWorkerPool.java    |   84 --
 .../BalanceableWorkerPoolImpl.java              |  185 ---
 .../policy/loadbalancing/BalancingStrategy.java |  622 ----------
 .../DefaultBalanceablePoolModel.java            |  280 -----
 .../loadbalancing/ItemsInContainersGroup.java   |   52 -
 .../ItemsInContainersGroupImpl.java             |  148 ---
 .../loadbalancing/LoadBalancingPolicy.java      |  343 ------
 .../loadbalancing/LocationConstraint.java       |   28 -
 .../brooklyn/policy/loadbalancing/Movable.java  |   51 -
 .../policy/loadbalancing/PolicyUtilForPool.java |   96 --
 .../policy/autoscaling/AutoScalerPolicy.java    | 1092 ++++++++++++++++++
 .../autoscaling/MaxPoolSizeReachedEvent.java    |  103 ++
 .../policy/autoscaling/ResizeOperator.java      |   31 +
 .../policy/autoscaling/SizeHistory.java         |  166 +++
 .../followthesun/DefaultFollowTheSunModel.java  |  328 ++++++
 .../policy/followthesun/FollowTheSunModel.java  |   56 +
 .../followthesun/FollowTheSunParameters.java    |   95 ++
 .../policy/followthesun/FollowTheSunPolicy.java |  282 +++++
 .../policy/followthesun/FollowTheSunPool.java   |   75 ++
 .../followthesun/FollowTheSunPoolImpl.java      |  178 +++
 .../followthesun/FollowTheSunStrategy.java      |  161 +++
 .../policy/followthesun/WeightedObject.java     |   71 ++
 .../policy/ha/AbstractFailureDetector.java      |  363 ++++++
 .../policy/ha/ConditionalSuspendPolicy.java     |  103 ++
 .../policy/ha/ConnectionFailureDetector.java    |  128 ++
 .../apache/brooklyn/policy/ha/HASensors.java    |   62 +
 .../policy/ha/ServiceFailureDetector.java       |  340 ++++++
 .../brooklyn/policy/ha/ServiceReplacer.java     |  216 ++++
 .../brooklyn/policy/ha/ServiceRestarter.java    |  165 +++
 .../policy/ha/SshMachineFailureDetector.java    |  103 ++
 .../loadbalancing/BalanceableContainer.java     |   51 +
 .../loadbalancing/BalanceablePoolModel.java     |   64 +
 .../loadbalancing/BalanceableWorkerPool.java    |   84 ++
 .../BalanceableWorkerPoolImpl.java              |  185 +++
 .../policy/loadbalancing/BalancingStrategy.java |  622 ++++++++++
 .../DefaultBalanceablePoolModel.java            |  280 +++++
 .../loadbalancing/ItemsInContainersGroup.java   |   52 +
 .../ItemsInContainersGroupImpl.java             |  148 +++
 .../loadbalancing/LoadBalancingPolicy.java      |  344 ++++++
 .../loadbalancing/LocationConstraint.java       |   28 +
 .../brooklyn/policy/loadbalancing/Movable.java  |   51 +
 .../policy/loadbalancing/PolicyUtilForPool.java |   96 ++
 .../autoscaling/AutoScalerPolicyMetricTest.java |  274 -----
 .../autoscaling/AutoScalerPolicyRebindTest.java |  137 ---
 .../AutoScalerPolicyReconfigurationTest.java    |  190 ---
 .../autoscaling/AutoScalerPolicyTest.java       |  649 -----------
 .../autoscaling/LocallyResizableEntity.java     |   73 --
 .../AbstractFollowTheSunPolicyTest.java         |  239 ----
 .../followthesun/FollowTheSunModelTest.java     |  195 ----
 .../FollowTheSunPolicySoakTest.java             |  274 -----
 .../followthesun/FollowTheSunPolicyTest.java    |  307 -----
 .../ha/ConnectionFailureDetectorTest.java       |  303 -----
 .../brooklyn/policy/ha/HaPolicyRebindTest.java  |  173 ---
 ...ServiceFailureDetectorStabilizationTest.java |  234 ----
 .../policy/ha/ServiceFailureDetectorTest.java   |  408 -------
 .../brooklyn/policy/ha/ServiceReplacerTest.java |  340 ------
 .../policy/ha/ServiceRestarterTest.java         |  190 ---
 .../AbstractLoadBalancingPolicyTest.java        |  253 ----
 .../BalanceableWorkerPoolTest.java              |  133 ---
 .../ItemsInContainersGroupTest.java             |  189 ---
 .../loadbalancing/LoadBalancingModelTest.java   |  115 --
 .../LoadBalancingPolicyConcurrencyTest.java     |  211 ----
 .../LoadBalancingPolicySoakTest.java            |  273 -----
 .../loadbalancing/LoadBalancingPolicyTest.java  |  397 -------
 .../loadbalancing/MockContainerEntity.java      |   61 -
 .../loadbalancing/MockContainerEntityImpl.java  |  208 ----
 .../policy/loadbalancing/MockItemEntity.java    |   46 -
 .../loadbalancing/MockItemEntityImpl.java       |  113 --
 .../autoscaling/AutoScalerPolicyMetricTest.java |  274 +++++
 .../autoscaling/AutoScalerPolicyRebindTest.java |  137 +++
 .../AutoScalerPolicyReconfigurationTest.java    |  190 +++
 .../autoscaling/AutoScalerPolicyTest.java       |  649 +++++++++++
 .../autoscaling/LocallyResizableEntity.java     |   73 ++
 .../AbstractFollowTheSunPolicyTest.java         |  239 ++++
 .../followthesun/FollowTheSunModelTest.java     |  195 ++++
 .../FollowTheSunPolicySoakTest.java             |  274 +++++
 .../followthesun/FollowTheSunPolicyTest.java    |  307 +++++
 .../ha/ConnectionFailureDetectorTest.java       |  303 +++++
 .../brooklyn/policy/ha/HaPolicyRebindTest.java  |  173 +++
 ...ServiceFailureDetectorStabilizationTest.java |  234 ++++
 .../policy/ha/ServiceFailureDetectorTest.java   |  407 +++++++
 .../brooklyn/policy/ha/ServiceReplacerTest.java |  340 ++++++
 .../policy/ha/ServiceRestarterTest.java         |  190 +++
 .../AbstractLoadBalancingPolicyTest.java        |  253 ++++
 .../BalanceableWorkerPoolTest.java              |  133 +++
 .../ItemsInContainersGroupTest.java             |  189 +++
 .../loadbalancing/LoadBalancingModelTest.java   |  113 ++
 .../LoadBalancingPolicyConcurrencyTest.java     |  211 ++++
 .../LoadBalancingPolicySoakTest.java            |  273 +++++
 .../loadbalancing/LoadBalancingPolicyTest.java  |  397 +++++++
 .../loadbalancing/MockContainerEntity.java      |   61 +
 .../loadbalancing/MockContainerEntityImpl.java  |  208 ++++
 .../policy/loadbalancing/MockItemEntity.java    |   46 +
 .../loadbalancing/MockItemEntityImpl.java       |  113 ++
 .../webapp/TomcatAutoScalerPolicyTest.java      |    3 +-
 .../app/ClusterWebServerDatabaseSample.java     |    2 +-
 .../BrooklynYamlTypeInstantiatorTest.java       |    2 +-
 .../brooklyn/JavaWebAppsIntegrationTest.java    |    4 +-
 .../brooklyn/catalog/CatalogYamlCombiTest.java  |    2 +-
 .../java-web-app-and-db-with-policy.yaml        |    2 +-
 .../resources/vanilla-bash-netcat-w-client.yaml |    4 +-
 .../main/resources/brooklyn/default.catalog.bom |   10 +-
 .../src/test/resources/couchbase-w-loadgen.yaml |    2 +-
 .../brooklyn/qa/load/SimulatedTheeTierApp.java  |    2 +-
 .../qa/longevity/webcluster/WebClusterApp.java  |    2 +-
 .../brooklyn/rest/domain/ConfigSummary.java     |    2 +-
 .../rest/resources/CatalogResourceTest.java     |    2 +-
 145 files changed, 12162 insertions(+), 12146 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/docs/guide/yaml/example_yaml/appserver-w-policy.yaml
----------------------------------------------------------------------
diff --git a/docs/guide/yaml/example_yaml/appserver-w-policy.yaml b/docs/guide/yaml/example_yaml/appserver-w-policy.yaml
index cada61f..d096818 100644
--- a/docs/guide/yaml/example_yaml/appserver-w-policy.yaml
+++ b/docs/guide/yaml/example_yaml/appserver-w-policy.yaml
@@ -12,7 +12,7 @@ services:
           brooklyn.example.db.url: $brooklyn:formatString("jdbc:%s%s?user=%s\\&password=%s",
               component("db").attributeWhenReady("datastore.url"), "visitors", "brooklyn", "br00k11n")
   brooklyn.policies:
-  - policyType: brooklyn.policy.autoscaling.AutoScalerPolicy
+  - policyType: org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy
     brooklyn.config:
       metric: $brooklyn:sensor("brooklyn.entity.webapp.DynamicWebAppCluster", "webapp.reqs.perSec.windowed.perNode")
       metricLowerBound: 10

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/docs/guide/yaml/example_yaml/vanilla-bash-netcat-restarter.yaml
----------------------------------------------------------------------
diff --git a/docs/guide/yaml/example_yaml/vanilla-bash-netcat-restarter.yaml b/docs/guide/yaml/example_yaml/vanilla-bash-netcat-restarter.yaml
index 1fd70c3..6752980 100644
--- a/docs/guide/yaml/example_yaml/vanilla-bash-netcat-restarter.yaml
+++ b/docs/guide/yaml/example_yaml/vanilla-bash-netcat-restarter.yaml
@@ -8,12 +8,12 @@ services:
     echo hello | nc -l 4321 &
     echo $! > $PID_FILE
   brooklyn.enrichers:
-  - type: brooklyn.policy.ha.ServiceFailureDetector
+  - type: org.apache.brooklyn.policy.ha.ServiceFailureDetector
     brooklyn.config:
       # wait 15s after service fails before propagating failure
       serviceFailedStabilizationDelay: 15s
   brooklyn.policies:
-  - type: brooklyn.policy.ha.ServiceRestarter
+  - type: org.apache.brooklyn.policy.ha.ServiceRestarter
     brooklyn.config:
       # repeated failures in a time window can cause the restarter to abort,
       # propagating the failure; a time window of 0 will mean it always restarts!

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/docs/guide/yaml/example_yaml/vanilla-bash-netcat-w-client.yaml
----------------------------------------------------------------------
diff --git a/docs/guide/yaml/example_yaml/vanilla-bash-netcat-w-client.yaml b/docs/guide/yaml/example_yaml/vanilla-bash-netcat-w-client.yaml
index fbf0505..4893968 100644
--- a/docs/guide/yaml/example_yaml/vanilla-bash-netcat-w-client.yaml
+++ b/docs/guide/yaml/example_yaml/vanilla-bash-netcat-w-client.yaml
@@ -14,13 +14,13 @@ services:
 
   # a failure detector and a service restarter work together
   brooklyn.enrichers:
-  - type: brooklyn.policy.ha.ServiceFailureDetector
+  - type: org.apache.brooklyn.policy.ha.ServiceFailureDetector
     brooklyn.config:
       # wait 15s after service fails before propagating failure
       serviceFailedStabilizationDelay: 15s
 
   brooklyn.policies:
-  - type: brooklyn.policy.ha.ServiceRestarter
+  - type: org.apache.brooklyn.policy.ha.ServiceRestarter
     brooklyn.config:
       # repeated failures in a time window can cause the restarter to abort,
       # propagating the failure; a time window of 0 will mean it always restarts!

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/docs/website/learnmore/catalog/locations/brooklyn.policy.followthesun.DefaultFollowTheSunModel$1.html
----------------------------------------------------------------------
diff --git a/docs/website/learnmore/catalog/locations/brooklyn.policy.followthesun.DefaultFollowTheSunModel$1.html b/docs/website/learnmore/catalog/locations/brooklyn.policy.followthesun.DefaultFollowTheSunModel$1.html
index 4bf0fd3..8172d3a 100644
--- a/docs/website/learnmore/catalog/locations/brooklyn.policy.followthesun.DefaultFollowTheSunModel$1.html
+++ b/docs/website/learnmore/catalog/locations/brooklyn.policy.followthesun.DefaultFollowTheSunModel$1.html
@@ -18,7 +18,7 @@ under the License.
 -->
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
 <head>
-    <title>Brooklyn Location - brooklyn.policy.followthesun.DefaultFollowTheSunModel$1</title>
+    <title>Brooklyn Location - org.apache.brooklyn.policy.followthesun.DefaultFollowTheSunModel$1</title>
     <meta http-equiv="content-type" content="text/html; charset=iso-8859-1"/>
     <link rel="stylesheet" href="../items.css" type="text/css" media="screen"/>
 </head>
@@ -30,7 +30,7 @@ under the License.
         </div>
     </div>
     <div id="content" class="objectContent">
-        <h1>brooklyn.policy.followthesun.DefaultFollowTheSunModel$1</h1>
+        <h1>org.apache.brooklyn.policy.followthesun.DefaultFollowTheSunModel$1</h1>
         <h2 class="typeLabel">Type:</h2><span id="type"></span>
 
         <h2>Config Keys</h2>
@@ -50,7 +50,7 @@ under the License.
 <script type="text/javascript">
     $(document).ready(function () {
         var item = $.grep((items.locations), function (entity) {
-            return entity.type == "brooklyn.policy.followthesun.DefaultFollowTheSunModel$1";
+            return entity.type == "org.apache.brooklyn.policy.followthesun.DefaultFollowTheSunModel$1";
         })[0];
         $("#type").html(item.type);
         item.config.forEach(function (element) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/CumulusRDFApplication.java
----------------------------------------------------------------------
diff --git a/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/CumulusRDFApplication.java b/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/CumulusRDFApplication.java
index da88bff..d79228d 100644
--- a/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/CumulusRDFApplication.java
+++ b/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/CumulusRDFApplication.java
@@ -63,9 +63,9 @@ import brooklyn.event.basic.DependentConfiguration;
 import org.apache.brooklyn.launcher.BrooklynLauncher;
 import org.apache.brooklyn.location.basic.PortRanges;
 
-import brooklyn.policy.ha.ServiceFailureDetector;
-import brooklyn.policy.ha.ServiceReplacer;
-import brooklyn.policy.ha.ServiceRestarter;
+import org.apache.brooklyn.policy.ha.ServiceFailureDetector;
+import org.apache.brooklyn.policy.ha.ServiceReplacer;
+import org.apache.brooklyn.policy.ha.ServiceRestarter;
 import brooklyn.util.CommandLineUtil;
 import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/HighAvailabilityCassandraCluster.java
----------------------------------------------------------------------
diff --git a/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/HighAvailabilityCassandraCluster.java b/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/HighAvailabilityCassandraCluster.java
index eda486f..0361ef4 100644
--- a/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/HighAvailabilityCassandraCluster.java
+++ b/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/HighAvailabilityCassandraCluster.java
@@ -36,9 +36,9 @@ import brooklyn.entity.basic.StartableApplication;
 
 import org.apache.brooklyn.launcher.BrooklynLauncher;
 
-import brooklyn.policy.ha.ServiceFailureDetector;
-import brooklyn.policy.ha.ServiceReplacer;
-import brooklyn.policy.ha.ServiceRestarter;
+import org.apache.brooklyn.policy.ha.ServiceFailureDetector;
+import org.apache.brooklyn.policy.ha.ServiceReplacer;
+import org.apache.brooklyn.policy.ha.ServiceRestarter;
 import brooklyn.util.CommandLineUtil;
 
 import com.google.common.collect.Lists;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/ResilientMongoDbApp.java
----------------------------------------------------------------------
diff --git a/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/ResilientMongoDbApp.java b/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/ResilientMongoDbApp.java
index fc0837c..1f2f4d3 100644
--- a/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/ResilientMongoDbApp.java
+++ b/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/ResilientMongoDbApp.java
@@ -39,9 +39,9 @@ import brooklyn.entity.group.DynamicCluster;
 
 import org.apache.brooklyn.launcher.BrooklynLauncher;
 
-import brooklyn.policy.ha.ServiceFailureDetector;
-import brooklyn.policy.ha.ServiceReplacer;
-import brooklyn.policy.ha.ServiceRestarter;
+import org.apache.brooklyn.policy.ha.ServiceFailureDetector;
+import org.apache.brooklyn.policy.ha.ServiceReplacer;
+import org.apache.brooklyn.policy.ha.ServiceRestarter;
 import brooklyn.util.CommandLineUtil;
 
 import com.google.common.collect.Lists;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/RiakClusterExample.java
----------------------------------------------------------------------
diff --git a/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/RiakClusterExample.java b/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/RiakClusterExample.java
index 0aaf152..3d434b7 100644
--- a/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/RiakClusterExample.java
+++ b/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/RiakClusterExample.java
@@ -36,8 +36,8 @@ import brooklyn.entity.basic.StartableApplication;
 
 import org.apache.brooklyn.launcher.BrooklynLauncher;
 
-import brooklyn.policy.ha.ServiceFailureDetector;
-import brooklyn.policy.ha.ServiceRestarter;
+import org.apache.brooklyn.policy.ha.ServiceFailureDetector;
+import org.apache.brooklyn.policy.ha.ServiceRestarter;
 import brooklyn.util.CommandLineUtil;
 
 import com.google.common.base.Preconditions;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/WideAreaCassandraCluster.java
----------------------------------------------------------------------
diff --git a/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/WideAreaCassandraCluster.java b/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/WideAreaCassandraCluster.java
index 7f010c6..16ad833 100644
--- a/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/WideAreaCassandraCluster.java
+++ b/examples/simple-nosql-cluster/src/main/java/org/apache/brooklyn/demo/WideAreaCassandraCluster.java
@@ -38,9 +38,9 @@ import brooklyn.entity.basic.StartableApplication;
 
 import org.apache.brooklyn.launcher.BrooklynLauncher;
 
-import brooklyn.policy.ha.ServiceFailureDetector;
-import brooklyn.policy.ha.ServiceReplacer;
-import brooklyn.policy.ha.ServiceRestarter;
+import org.apache.brooklyn.policy.ha.ServiceFailureDetector;
+import org.apache.brooklyn.policy.ha.ServiceReplacer;
+import org.apache.brooklyn.policy.ha.ServiceRestarter;
 import brooklyn.util.CommandLineUtil;
 
 import com.google.common.collect.Lists;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-nosql-cluster/src/main/resources/org/apache/brooklyn/demo/ha-cassandra-cluster.yaml
----------------------------------------------------------------------
diff --git a/examples/simple-nosql-cluster/src/main/resources/org/apache/brooklyn/demo/ha-cassandra-cluster.yaml b/examples/simple-nosql-cluster/src/main/resources/org/apache/brooklyn/demo/ha-cassandra-cluster.yaml
index d1c6ab4..a260e74 100644
--- a/examples/simple-nosql-cluster/src/main/resources/org/apache/brooklyn/demo/ha-cassandra-cluster.yaml
+++ b/examples/simple-nosql-cluster/src/main/resources/org/apache/brooklyn/demo/ha-cassandra-cluster.yaml
@@ -38,8 +38,8 @@ services:
       $brooklyn:entitySpec:
         type: org.apache.brooklyn.entity.nosql.cassandra.CassandraNode
         brookyn.policies:
-        - type: brooklyn.policy.ha.ServiceRestarter
+        - type: org.apache.brooklyn.policy.ha.ServiceRestarter
         brooklyn.enrichers:
-        - type: brooklyn.policy.ha.ServiceFailureDetector
+        - type: org.apache.brooklyn.policy.ha.ServiceFailureDetector
   brooklyn.policies:
-  - type: brooklyn.policy.ha.ServiceReplacer
+  - type: org.apache.brooklyn.policy.ha.ServiceReplacer

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-nosql-cluster/src/main/resources/org/apache/brooklyn/demo/wide-area-cassandra-cluster.yaml
----------------------------------------------------------------------
diff --git a/examples/simple-nosql-cluster/src/main/resources/org/apache/brooklyn/demo/wide-area-cassandra-cluster.yaml b/examples/simple-nosql-cluster/src/main/resources/org/apache/brooklyn/demo/wide-area-cassandra-cluster.yaml
index 0cf39da..91b785c 100644
--- a/examples/simple-nosql-cluster/src/main/resources/org/apache/brooklyn/demo/wide-area-cassandra-cluster.yaml
+++ b/examples/simple-nosql-cluster/src/main/resources/org/apache/brooklyn/demo/wide-area-cassandra-cluster.yaml
@@ -34,8 +34,8 @@ services:
       $brooklyn:entitySpec:
         type: org.apache.brooklyn.entity.nosql.cassandra.CassandraNode
         brookyn.policies:
-        - type: brooklyn.policy.ha.ServiceRestarter
+        - type: org.apache.brooklyn.policy.ha.ServiceRestarter
         brooklyn.enrichers:
-        - type: brooklyn.policy.ha.ServiceFailureDetector
+        - type: org.apache.brooklyn.policy.ha.ServiceFailureDetector
     brooklyn.policies:
-    - type: brooklyn.policy.ha.ServiceReplacer
+    - type: org.apache.brooklyn.policy.ha.ServiceReplacer

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExample.java
----------------------------------------------------------------------
diff --git a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExample.java b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExample.java
index 7084b74..88f3e2f 100644
--- a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExample.java
+++ b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExample.java
@@ -46,7 +46,7 @@ import org.apache.brooklyn.entity.webapp.WebAppServiceConstants;
 import org.apache.brooklyn.launcher.BrooklynLauncher;
 
 import org.apache.brooklyn.location.basic.PortRanges;
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
 import brooklyn.util.CommandLineUtil;
 
 import com.google.common.collect.ImmutableMap;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleApp.java
----------------------------------------------------------------------
diff --git a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleApp.java b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleApp.java
index 441999f..f9366dc 100644
--- a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleApp.java
+++ b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleApp.java
@@ -53,7 +53,8 @@ import brooklyn.event.basic.Sensors;
 import org.apache.brooklyn.launcher.BrooklynLauncher;
 import org.apache.brooklyn.location.basic.PortRanges;
 
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
+
 import brooklyn.util.CommandLineUtil;
 
 import com.google.common.collect.ImmutableMap;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleGroovy.groovy
----------------------------------------------------------------------
diff --git a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleGroovy.groovy b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleGroovy.groovy
index 9f965b9..8bade65 100644
--- a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleGroovy.groovy
+++ b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterDatabaseExampleGroovy.groovy
@@ -29,10 +29,13 @@ import org.slf4j.LoggerFactory
 import brooklyn.entity.basic.AbstractApplication
 import brooklyn.entity.basic.Entities
 import brooklyn.entity.database.mysql.MySqlNode
+
 import org.apache.brooklyn.api.entity.proxying.EntitySpec
 import org.apache.brooklyn.entity.webapp.ControlledDynamicWebAppCluster
 import org.apache.brooklyn.entity.webapp.DynamicWebAppCluster
-import brooklyn.policy.autoscaling.AutoScalerPolicy
+
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy
+
 import brooklyn.util.CommandLineUtil
 
 import com.google.common.collect.Lists

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterExample.java
----------------------------------------------------------------------
diff --git a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterExample.java b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterExample.java
index 2f6f2f9..95197ed 100644
--- a/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterExample.java
+++ b/examples/simple-web-cluster/src/main/java/org/apache/brooklyn/demo/WebClusterExample.java
@@ -34,7 +34,7 @@ import org.apache.brooklyn.entity.webapp.DynamicWebAppCluster;
 import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server;
 import org.apache.brooklyn.launcher.BrooklynLauncher;
 
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
 import brooklyn.util.CommandLineUtil;
 
 import com.google.common.collect.Lists;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/examples/simple-web-cluster/src/test/java/org/apache/brooklyn/demo/RebindWebClusterDatabaseExampleAppIntegrationTest.java
----------------------------------------------------------------------
diff --git a/examples/simple-web-cluster/src/test/java/org/apache/brooklyn/demo/RebindWebClusterDatabaseExampleAppIntegrationTest.java b/examples/simple-web-cluster/src/test/java/org/apache/brooklyn/demo/RebindWebClusterDatabaseExampleAppIntegrationTest.java
index 38f7059..2b1537c 100644
--- a/examples/simple-web-cluster/src/test/java/org/apache/brooklyn/demo/RebindWebClusterDatabaseExampleAppIntegrationTest.java
+++ b/examples/simple-web-cluster/src/test/java/org/apache/brooklyn/demo/RebindWebClusterDatabaseExampleAppIntegrationTest.java
@@ -56,7 +56,7 @@ import brooklyn.entity.group.DynamicCluster;
 import brooklyn.entity.java.JavaEntityMethods;
 import brooklyn.entity.rebind.RebindOptions;
 import brooklyn.entity.rebind.RebindTestFixture;
-import brooklyn.policy.autoscaling.AutoScalerPolicy;
+import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy;
 import brooklyn.test.Asserts;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.time.Duration;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/autoscaling/AutoScalerPolicy.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/autoscaling/AutoScalerPolicy.java b/policy/src/main/java/brooklyn/policy/autoscaling/AutoScalerPolicy.java
deleted file mode 100644
index 18480bd..0000000
--- a/policy/src/main/java/brooklyn/policy/autoscaling/AutoScalerPolicy.java
+++ /dev/null
@@ -1,1090 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.autoscaling;
-
-import static brooklyn.util.JavaGroovyEquivalents.groovyTruth;
-import static com.google.common.base.Preconditions.checkNotNull;
-import groovy.lang.Closure;
-
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.brooklyn.api.catalog.Catalog;
-import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.basic.EntityLocal;
-import org.apache.brooklyn.api.event.AttributeSensor;
-import org.apache.brooklyn.api.event.Sensor;
-import org.apache.brooklyn.api.event.SensorEvent;
-import org.apache.brooklyn.api.event.SensorEventListener;
-import org.apache.brooklyn.api.policy.PolicySpec;
-import org.apache.brooklyn.core.policy.basic.AbstractPolicy;
-import org.apache.brooklyn.core.util.flags.SetFromFlag;
-import org.apache.brooklyn.core.util.flags.TypeCoercions;
-import org.apache.brooklyn.core.util.task.Tasks;
-
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.BrooklynTaskTags;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.trait.Resizable;
-import brooklyn.entity.trait.Startable;
-import brooklyn.event.basic.BasicConfigKey;
-import brooklyn.event.basic.BasicNotificationSensor;
-import brooklyn.policy.autoscaling.SizeHistory.WindowSummary;
-import brooklyn.policy.loadbalancing.LoadBalancingPolicy;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.time.Duration;
-
-import com.google.common.base.Function;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Throwables;
-import com.google.common.reflect.TypeToken;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-
-
-/**
- * Policy that is attached to a {@link Resizable} entity and dynamically adjusts its size in response to
- * emitted {@code POOL_COLD} and {@code POOL_HOT} events. Alternatively, the policy can be configured to
- * keep a given metric within a required range.
- * <p>
- * TThis policy does not itself determine whether the pool is hot or cold, but instead relies on these 
- * events being emitted by the monitored entity itself, or by another policy that is attached to it; see, 
- * for example, {@link LoadBalancingPolicy}.)
- */
-@SuppressWarnings({"rawtypes", "unchecked"})
-@Catalog(name="Auto-scaler", description="Policy that is attached to a Resizable entity and dynamically "
-        + "adjusts its size in response to either keep a metric within a given range, or in response to "
-        + "POOL_COLD and POOL_HOT events")
-public class AutoScalerPolicy extends AbstractPolicy {
-    
-    private static final Logger LOG = LoggerFactory.getLogger(AutoScalerPolicy.class);
-
-    public static Builder builder() {
-        return new Builder();
-    }
-    
-    public static class Builder {
-        private String id;
-        private String name;
-        private AttributeSensor<? extends Number> metric;
-        private Entity entityWithMetric;
-        private Number metricUpperBound;
-        private Number metricLowerBound;
-        private int minPoolSize = 1;
-        private int maxPoolSize = Integer.MAX_VALUE;
-        private Integer resizeDownIterationIncrement;
-        private Integer resizeDownIterationMax;
-        private Integer resizeUpIterationIncrement;
-        private Integer resizeUpIterationMax;
-        private Duration minPeriodBetweenExecs;
-        private Duration resizeUpStabilizationDelay;
-        private Duration resizeDownStabilizationDelay;
-        private ResizeOperator resizeOperator;
-        private Function<Entity,Integer> currentSizeOperator;
-        private BasicNotificationSensor<?> poolHotSensor;
-        private BasicNotificationSensor<?> poolColdSensor;
-        private BasicNotificationSensor<?> poolOkSensor;
-        private BasicNotificationSensor<? super MaxPoolSizeReachedEvent> maxSizeReachedSensor;
-        private Duration maxReachedNotificationDelay;
-        
-        public Builder id(String val) {
-            this.id = val; return this;
-        }
-        public Builder name(String val) {
-            this.name = val; return this;
-        }
-        public Builder metric(AttributeSensor<? extends Number> val) {
-            this.metric = val; return this;
-        }
-        public Builder entityWithMetric(Entity val) {
-            this.entityWithMetric = val; return this;
-        }
-        public Builder metricLowerBound(Number val) {
-            this.metricLowerBound = val; return this;
-        }
-        public Builder metricUpperBound(Number val) {
-            this.metricUpperBound = val; return this;
-        }
-        public Builder metricRange(Number min, Number max) {
-            metricLowerBound = checkNotNull(min);
-            metricUpperBound = checkNotNull(max);
-            return this;
-        }
-        public Builder minPoolSize(int val) {
-            this.minPoolSize = val; return this;
-        }
-        public Builder maxPoolSize(int val) {
-            this.maxPoolSize = val; return this;
-        }
-        public Builder sizeRange(int min, int max) {
-            minPoolSize = min;
-            maxPoolSize = max;
-            return this;
-        }
-        
-        public Builder resizeUpIterationIncrement(Integer val) {
-            this.resizeUpIterationIncrement = val; return this;
-        }
-        public Builder resizeUpIterationMax(Integer val) {
-            this.resizeUpIterationMax = val; return this;
-        }
-        public Builder resizeDownIterationIncrement(Integer val) {
-            this.resizeUpIterationIncrement = val; return this;
-        }
-        public Builder resizeDownIterationMax(Integer val) {
-            this.resizeUpIterationMax = val; return this;
-        }
-
-        public Builder minPeriodBetweenExecs(Duration val) {
-            this.minPeriodBetweenExecs = val; return this;
-        }
-        public Builder resizeUpStabilizationDelay(Duration val) {
-            this.resizeUpStabilizationDelay = val; return this;
-        }
-        public Builder resizeDownStabilizationDelay(Duration val) {
-            this.resizeDownStabilizationDelay = val; return this;
-        }
-        public Builder resizeOperator(ResizeOperator val) {
-            this.resizeOperator = val; return this;
-        }
-        public Builder currentSizeOperator(Function<Entity, Integer> val) {
-            this.currentSizeOperator = val; return this;
-        }
-        public Builder poolHotSensor(BasicNotificationSensor<?> val) {
-            this.poolHotSensor = val; return this;
-        }
-        public Builder poolColdSensor(BasicNotificationSensor<?> val) {
-            this.poolColdSensor = val; return this;
-        }
-        public Builder poolOkSensor(BasicNotificationSensor<?> val) {
-            this.poolOkSensor = val; return this;
-        }
-        public Builder maxSizeReachedSensor(BasicNotificationSensor<? super MaxPoolSizeReachedEvent> val) {
-            this.maxSizeReachedSensor = val; return this;
-        }
-        public Builder maxReachedNotificationDelay(Duration val) {
-            this.maxReachedNotificationDelay = val; return this;
-        }
-        public AutoScalerPolicy build() {
-            return new AutoScalerPolicy(toFlags());
-        }
-        public PolicySpec<AutoScalerPolicy> buildSpec() {
-            return PolicySpec.create(AutoScalerPolicy.class)
-                    .configure(toFlags());
-        }
-        private Map<String,?> toFlags() {
-            return MutableMap.<String,Object>builder()
-                    .putIfNotNull("id", id)
-                    .putIfNotNull("name", name)
-                    .putIfNotNull("metric", metric)
-                    .putIfNotNull("entityWithMetric", entityWithMetric)
-                    .putIfNotNull("metricUpperBound", metricUpperBound)
-                    .putIfNotNull("metricLowerBound", metricLowerBound)
-                    .putIfNotNull("minPoolSize", minPoolSize)
-                    .putIfNotNull("maxPoolSize", maxPoolSize)
-                    .putIfNotNull("resizeUpIterationMax", resizeUpIterationMax)
-                    .putIfNotNull("resizeUpIterationIncrement", resizeUpIterationIncrement)
-                    .putIfNotNull("resizeDownIterationMax", resizeDownIterationMax)
-                    .putIfNotNull("resizeDownIterationIncrement", resizeDownIterationIncrement)
-                    .putIfNotNull("minPeriodBetweenExecs", minPeriodBetweenExecs)
-                    .putIfNotNull("resizeUpStabilizationDelay", resizeUpStabilizationDelay)
-                    .putIfNotNull("resizeDownStabilizationDelay", resizeDownStabilizationDelay)
-                    .putIfNotNull("resizeOperator", resizeOperator)
-                    .putIfNotNull("currentSizeOperator", currentSizeOperator)
-                    .putIfNotNull("poolHotSensor", poolHotSensor)
-                    .putIfNotNull("poolColdSensor", poolColdSensor)
-                    .putIfNotNull("poolOkSensor", poolOkSensor)
-                    .putIfNotNull("maxSizeReachedSensor", maxSizeReachedSensor)
-                    .putIfNotNull("maxReachedNotificationDelay", maxReachedNotificationDelay)
-                    .build();
-        }
-    }
-    
-    // TODO Is there a nicer pattern for registering such type-coercions? 
-    // Can't put it in the ResizeOperator interface, nor in core TypeCoercions class because interface is defined in policy/.
-    static {
-        TypeCoercions.registerAdapter(Closure.class, ResizeOperator.class, new Function<Closure,ResizeOperator>() {
-            @Override
-            public ResizeOperator apply(final Closure closure) {
-                return new ResizeOperator() {
-                    @Override public Integer resize(Entity entity, Integer input) {
-                        return (Integer) closure.call(entity, input);
-                    }
-                };
-            }
-        });
-    }
-    
-    // Pool workrate notifications.
-    public static BasicNotificationSensor<Map> DEFAULT_POOL_HOT_SENSOR = new BasicNotificationSensor<Map>(
-        Map.class, "resizablepool.hot", "Pool is over-utilized; it has insufficient resource for current workload");
-    public static BasicNotificationSensor<Map> DEFAULT_POOL_COLD_SENSOR = new BasicNotificationSensor<Map>(
-        Map.class, "resizablepool.cold", "Pool is under-utilized; it has too much resource for current workload");
-    public static BasicNotificationSensor<Map> DEFAULT_POOL_OK_SENSOR = new BasicNotificationSensor<Map>(
-        Map.class, "resizablepool.cold", "Pool utilization is ok; the available resources are fine for the current workload");
-
-    /**
-     * A convenience for policies that want to register a {@code builder.maxSizeReachedSensor(sensor)}.
-     * Note that this "default" is not set automatically; the default is for no sensor to be used (so
-     * no events emitted).
-     */
-    public static BasicNotificationSensor<MaxPoolSizeReachedEvent> DEFAULT_MAX_SIZE_REACHED_SENSOR = new BasicNotificationSensor<MaxPoolSizeReachedEvent>(
-            MaxPoolSizeReachedEvent.class, "resizablepool.maxSizeReached", "Consistently wanted to resize the pool above the max allowed size");
-
-    public static final String POOL_CURRENT_SIZE_KEY = "pool.current.size";
-    public static final String POOL_HIGH_THRESHOLD_KEY = "pool.high.threshold";
-    public static final String POOL_LOW_THRESHOLD_KEY = "pool.low.threshold";
-    public static final String POOL_CURRENT_WORKRATE_KEY = "pool.current.workrate";
-    
-    @SuppressWarnings("serial")
-    @SetFromFlag("metric")
-    public static final ConfigKey<AttributeSensor<? extends Number>> METRIC = BasicConfigKey.builder(new TypeToken<AttributeSensor<? extends Number>>() {})
-            .name("autoscaler.metric")
-            .build();
-
-    @SetFromFlag("entityWithMetric")
-    public static final ConfigKey<Entity> ENTITY_WITH_METRIC = BasicConfigKey.builder(Entity.class)
-            .name("autoscaler.entityWithMetric")
-            .build();
-    
-    @SetFromFlag("metricLowerBound")
-    public static final ConfigKey<Number> METRIC_LOWER_BOUND = BasicConfigKey.builder(Number.class)
-            .name("autoscaler.metricLowerBound")
-            .reconfigurable(true)
-            .build();
-    
-    @SetFromFlag("metricUpperBound")
-    public static final ConfigKey<Number> METRIC_UPPER_BOUND = BasicConfigKey.builder(Number.class)
-            .name("autoscaler.metricUpperBound")
-            .reconfigurable(true)
-            .build();
-    
-    @SetFromFlag("resizeUpIterationIncrement")
-    public static final ConfigKey<Integer> RESIZE_UP_ITERATION_INCREMENT = BasicConfigKey.builder(Integer.class)
-            .name("autoscaler.resizeUpIterationIncrement")
-            .description("Batch size for resizing up; the size will be increased by a multiple of this value")
-            .defaultValue(1)
-            .reconfigurable(true)
-            .build();
-    @SetFromFlag("resizeUpIterationMax")
-    public static final ConfigKey<Integer> RESIZE_UP_ITERATION_MAX = BasicConfigKey.builder(Integer.class)
-            .name("autoscaler.resizeUpIterationMax")
-            .defaultValue(Integer.MAX_VALUE)
-            .description("Maximum change to the size on a single iteration when scaling up")
-            .reconfigurable(true)
-            .build();
-    @SetFromFlag("resizeDownIterationIncrement")
-    public static final ConfigKey<Integer> RESIZE_DOWN_ITERATION_INCREMENT = BasicConfigKey.builder(Integer.class)
-            .name("autoscaler.resizeDownIterationIncrement")
-            .description("Batch size for resizing down; the size will be decreased by a multiple of this value")
-            .defaultValue(1)
-            .reconfigurable(true)
-            .build();
-    @SetFromFlag("resizeDownIterationMax")
-    public static final ConfigKey<Integer> RESIZE_DOWN_ITERATION_MAX = BasicConfigKey.builder(Integer.class)
-            .name("autoscaler.resizeDownIterationMax")
-            .defaultValue(Integer.MAX_VALUE)
-            .description("Maximum change to the size on a single iteration when scaling down")
-            .reconfigurable(true)
-            .build();
-
-    @SetFromFlag("minPeriodBetweenExecs")
-    public static final ConfigKey<Duration> MIN_PERIOD_BETWEEN_EXECS = BasicConfigKey.builder(Duration.class)
-            .name("autoscaler.minPeriodBetweenExecs")
-            .defaultValue(Duration.millis(100))
-            .build();
-    
-    @SetFromFlag("resizeUpStabilizationDelay")
-    public static final ConfigKey<Duration> RESIZE_UP_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
-            .name("autoscaler.resizeUpStabilizationDelay")
-            .defaultValue(Duration.ZERO)
-            .reconfigurable(true)
-            .build();
-    
-    @SetFromFlag("resizeDownStabilizationDelay")
-    public static final ConfigKey<Duration> RESIZE_DOWN_STABILIZATION_DELAY = BasicConfigKey.builder(Duration.class)
-            .name("autoscaler.resizeDownStabilizationDelay")
-            .defaultValue(Duration.ZERO)
-            .reconfigurable(true)
-            .build();
-
-    @SetFromFlag("minPoolSize")
-    public static final ConfigKey<Integer> MIN_POOL_SIZE = BasicConfigKey.builder(Integer.class)
-            .name("autoscaler.minPoolSize")
-            .defaultValue(1)
-            .reconfigurable(true)
-            .build();
-    
-    @SetFromFlag("maxPoolSize")
-    public static final ConfigKey<Integer> MAX_POOL_SIZE = BasicConfigKey.builder(Integer.class)
-            .name("autoscaler.maxPoolSize")
-            .defaultValue(Integer.MAX_VALUE)
-            .reconfigurable(true)
-            .build();
-
-    @SetFromFlag("resizeOperator")
-    public static final ConfigKey<ResizeOperator> RESIZE_OPERATOR = BasicConfigKey.builder(ResizeOperator.class)
-            .name("autoscaler.resizeOperator")
-            .defaultValue(new ResizeOperator() {
-                    public Integer resize(Entity entity, Integer desiredSize) {
-                        return ((Resizable)entity).resize(desiredSize);
-                    }})
-            .build();
-    
-    @SuppressWarnings("serial")
-    @SetFromFlag("currentSizeOperator")
-    public static final ConfigKey<Function<Entity,Integer>> CURRENT_SIZE_OPERATOR = BasicConfigKey.builder(new TypeToken<Function<Entity,Integer>>() {})
-            .name("autoscaler.currentSizeOperator")
-            .defaultValue(new Function<Entity,Integer>() {
-                    public Integer apply(Entity entity) {
-                        return ((Resizable)entity).getCurrentSize();
-                    }})
-            .build();
-
-    @SuppressWarnings("serial")
-    @SetFromFlag("poolHotSensor")
-    public static final ConfigKey<BasicNotificationSensor<? extends Map>> POOL_HOT_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? extends Map>>() {})
-            .name("autoscaler.poolHotSensor")
-            .defaultValue(DEFAULT_POOL_HOT_SENSOR)
-            .build();
-
-    @SuppressWarnings("serial")
-    @SetFromFlag("poolColdSensor")
-    public static final ConfigKey<BasicNotificationSensor<? extends Map>> POOL_COLD_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? extends Map>>() {})
-            .name("autoscaler.poolColdSensor")
-            .defaultValue(DEFAULT_POOL_COLD_SENSOR)
-            .build();
-
-    @SuppressWarnings("serial")
-    @SetFromFlag("poolOkSensor")
-    public static final ConfigKey<BasicNotificationSensor<? extends Map>> POOL_OK_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? extends Map>>() {})
-            .name("autoscaler.poolOkSensor")
-            .defaultValue(DEFAULT_POOL_OK_SENSOR)
-            .build();
-
-    @SuppressWarnings("serial")
-    @SetFromFlag("maxSizeReachedSensor")
-    public static final ConfigKey<BasicNotificationSensor<? super MaxPoolSizeReachedEvent>> MAX_SIZE_REACHED_SENSOR = BasicConfigKey.builder(new TypeToken<BasicNotificationSensor<? super MaxPoolSizeReachedEvent>>() {})
-            .name("autoscaler.maxSizeReachedSensor")
-            .description("Sensor for which a notification will be emitted (on the associated entity) when " +
-                    "we consistently wanted to resize the pool above the max allowed size, for " +
-                    "maxReachedNotificationDelay milliseconds")
-            .build();
-    
-    @SetFromFlag("maxReachedNotificationDelay")
-    public static final ConfigKey<Duration> MAX_REACHED_NOTIFICATION_DELAY = BasicConfigKey.builder(Duration.class)
-            .name("autoscaler.maxReachedNotificationDelay")
-            .description("Time that we consistently wanted to go above the maxPoolSize for, after which the " +
-                    "maxSizeReachedSensor (if any) will be emitted")
-            .defaultValue(Duration.ZERO)
-            .build();
-    
-    private Entity poolEntity;
-    
-    private final AtomicBoolean executorQueued = new AtomicBoolean(false);
-    private volatile long executorTime = 0;
-    private volatile ScheduledExecutorService executor;
-
-    private SizeHistory recentUnboundedResizes;
-
-    private SizeHistory recentDesiredResizes;
-    
-    private long maxReachedLastNotifiedTime;
-    
-    private final SensorEventListener<Map> utilizationEventHandler = new SensorEventListener<Map>() {
-        public void onEvent(SensorEvent<Map> event) {
-            Map<String, ?> properties = (Map<String, ?>) event.getValue();
-            Sensor<?> sensor = event.getSensor();
-            
-            if (sensor.equals(getPoolColdSensor())) {
-                onPoolCold(properties);
-            } else if (sensor.equals(getPoolHotSensor())) {
-                onPoolHot(properties);
-            } else if (sensor.equals(getPoolOkSensor())) {
-                onPoolOk(properties);
-            } else {
-                throw new IllegalStateException("Unexpected sensor type: "+sensor+"; event="+event);
-            }
-        }
-    };
-
-    private final SensorEventListener<Number> metricEventHandler = new SensorEventListener<Number>() {
-        public void onEvent(SensorEvent<Number> event) {
-            assert event.getSensor().equals(getMetric());
-            onMetricChanged(event.getValue());
-        }
-    };
-
-    public AutoScalerPolicy() {
-        this(MutableMap.<String,Object>of());
-    }
-    
-    public AutoScalerPolicy(Map<String,?> props) {
-        super(props);
-    }
-
-    @Override
-    public void init() {
-        doInit();
-    }
-
-    @Override
-    public void rebind() {
-        doInit();
-    }
-    
-    protected void doInit() {
-        long maxReachedNotificationDelay = getMaxReachedNotificationDelay().toMilliseconds();
-        recentUnboundedResizes = new SizeHistory(maxReachedNotificationDelay);
-        
-        long maxResizeStabilizationDelay = Math.max(getResizeUpStabilizationDelay().toMilliseconds(), getResizeDownStabilizationDelay().toMilliseconds());
-        recentDesiredResizes = new SizeHistory(maxResizeStabilizationDelay);
-        
-        // TODO Should re-use the execution manager's thread pool, somehow
-        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
-    }
-
-    public void setMetricLowerBound(Number val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing metricLowerBound from {} to {}", new Object[] {this, getMetricLowerBound(), val});
-        config().set(METRIC_LOWER_BOUND, checkNotNull(val));
-    }
-    
-    public void setMetricUpperBound(Number val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing metricUpperBound from {} to {}", new Object[] {this, getMetricUpperBound(), val});
-        config().set(METRIC_UPPER_BOUND, checkNotNull(val));
-    }
-    
-    private <T> void setOrDefault(ConfigKey<T> key, T val) {
-        if (val==null) val = key.getDefaultValue();
-        config().set(key, val);
-    }
-    public int getResizeUpIterationIncrement() { return getConfig(RESIZE_UP_ITERATION_INCREMENT); }
-    public void setResizeUpIterationIncrement(Integer val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeUpIterationIncrement from {} to {}", new Object[] {this, getResizeUpIterationIncrement(), val});
-        setOrDefault(RESIZE_UP_ITERATION_INCREMENT, val);
-    }
-    public int getResizeDownIterationIncrement() { return getConfig(RESIZE_DOWN_ITERATION_INCREMENT); }
-    public void setResizeDownIterationIncrement(Integer val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeDownIterationIncrement from {} to {}", new Object[] {this, getResizeDownIterationIncrement(), val});
-        setOrDefault(RESIZE_DOWN_ITERATION_INCREMENT, val);
-    }
-    public int getResizeUpIterationMax() { return getConfig(RESIZE_UP_ITERATION_MAX); }
-    public void setResizeUpIterationMax(Integer val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeUpIterationMax from {} to {}", new Object[] {this, getResizeUpIterationMax(), val});
-        setOrDefault(RESIZE_UP_ITERATION_MAX, val);
-    }
-    public int getResizeDownIterationMax() { return getConfig(RESIZE_DOWN_ITERATION_MAX); }
-    public void setResizeDownIterationMax(Integer val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeDownIterationMax from {} to {}", new Object[] {this, getResizeDownIterationMax(), val});
-        setOrDefault(RESIZE_DOWN_ITERATION_MAX, val);
-    }
-
-    public void setMinPeriodBetweenExecs(Duration val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing minPeriodBetweenExecs from {} to {}", new Object[] {this, getMinPeriodBetweenExecs(), val});
-        config().set(MIN_PERIOD_BETWEEN_EXECS, val);
-    }
-
-    public void setResizeUpStabilizationDelay(Duration val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeUpStabilizationDelay from {} to {}", new Object[] {this, getResizeUpStabilizationDelay(), val});
-        config().set(RESIZE_UP_STABILIZATION_DELAY, val);
-    }
-    
-    public void setResizeDownStabilizationDelay(Duration val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing resizeDownStabilizationDelay from {} to {}", new Object[] {this, getResizeDownStabilizationDelay(), val});
-        config().set(RESIZE_DOWN_STABILIZATION_DELAY, val);
-    }
-    
-    public void setMinPoolSize(int val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing minPoolSize from {} to {}", new Object[] {this, getMinPoolSize(), val});
-        config().set(MIN_POOL_SIZE, val);
-    }
-    
-    public void setMaxPoolSize(int val) {
-        if (LOG.isInfoEnabled()) LOG.info("{} changing maxPoolSize from {} to {}", new Object[] {this, getMaxPoolSize(), val});
-        config().set(MAX_POOL_SIZE, val);
-    }
-    
-    private AttributeSensor<? extends Number> getMetric() {
-        return getConfig(METRIC);
-    }
-
-    private Entity getEntityWithMetric() {
-        return getConfig(ENTITY_WITH_METRIC);
-    }
-    
-    private Number getMetricLowerBound() {
-        return getConfig(METRIC_LOWER_BOUND);
-    }
-    
-    private Number getMetricUpperBound() {
-        return getConfig(METRIC_UPPER_BOUND);
-    }
-    
-    private Duration getMinPeriodBetweenExecs() {
-        return getConfig(MIN_PERIOD_BETWEEN_EXECS);
-    }
-    
-    private Duration getResizeUpStabilizationDelay() {
-        return getConfig(RESIZE_UP_STABILIZATION_DELAY);
-    }
-    
-    private Duration getResizeDownStabilizationDelay() {
-        return getConfig(RESIZE_DOWN_STABILIZATION_DELAY);
-    }
-    
-    private int getMinPoolSize() {
-        return getConfig(MIN_POOL_SIZE);
-    }
-    
-    private int getMaxPoolSize() {
-        return getConfig(MAX_POOL_SIZE);
-    }
-    
-    private ResizeOperator getResizeOperator() {
-        return getConfig(RESIZE_OPERATOR);
-    }
-    
-    private Function<Entity,Integer> getCurrentSizeOperator() {
-        return getConfig(CURRENT_SIZE_OPERATOR);
-    }
-    
-    private BasicNotificationSensor<? extends Map> getPoolHotSensor() {
-        return getConfig(POOL_HOT_SENSOR);
-    }
-    
-    private BasicNotificationSensor<? extends Map> getPoolColdSensor() {
-        return getConfig(POOL_COLD_SENSOR);
-    }
-    
-    private BasicNotificationSensor<? extends Map> getPoolOkSensor() {
-        return getConfig(POOL_OK_SENSOR);
-    }
-    
-    private BasicNotificationSensor<? super MaxPoolSizeReachedEvent> getMaxSizeReachedSensor() {
-        return getConfig(MAX_SIZE_REACHED_SENSOR);
-    }
-    
-    private Duration getMaxReachedNotificationDelay() {
-        return getConfig(MAX_REACHED_NOTIFICATION_DELAY);
-    }
-
-    @Override
-    protected <T> void doReconfigureConfig(ConfigKey<T> key, T val) {
-        if (key.equals(RESIZE_UP_STABILIZATION_DELAY)) {
-            Duration maxResizeStabilizationDelay = Duration.max((Duration)val, getResizeDownStabilizationDelay());
-            recentDesiredResizes.setWindowSize(maxResizeStabilizationDelay);
-        } else if (key.equals(RESIZE_DOWN_STABILIZATION_DELAY)) {
-            Duration maxResizeStabilizationDelay = Duration.max((Duration)val, getResizeUpStabilizationDelay());
-            recentDesiredResizes.setWindowSize(maxResizeStabilizationDelay);
-        } else if (key.equals(METRIC_LOWER_BOUND)) {
-            // TODO If recorded what last metric value was then we could recalculate immediately
-            // Rely on next metric-change to trigger recalculation; 
-            // and same for those below...
-        } else if (key.equals(METRIC_UPPER_BOUND)) {
-            // see above
-        } else if (key.equals(RESIZE_UP_ITERATION_INCREMENT) || key.equals(RESIZE_UP_ITERATION_MAX) || key.equals(RESIZE_DOWN_ITERATION_INCREMENT) || key.equals(RESIZE_DOWN_ITERATION_MAX)) {
-            // no special actions needed
-        } else if (key.equals(MIN_POOL_SIZE)) {
-            int newMin = (Integer) val;
-            if (newMin > getConfig(MAX_POOL_SIZE)) {
-                throw new IllegalArgumentException("Min pool size "+val+" must not be greater than max pool size "+getConfig(MAX_POOL_SIZE));
-            }
-            onPoolSizeLimitsChanged(newMin, getConfig(MAX_POOL_SIZE));
-        } else if (key.equals(MAX_POOL_SIZE)) {
-            int newMax = (Integer) val;
-            if (newMax < getConfig(MIN_POOL_SIZE)) {
-                throw new IllegalArgumentException("Min pool size "+val+" must not be greater than max pool size "+getConfig(MAX_POOL_SIZE));
-            }
-            onPoolSizeLimitsChanged(getConfig(MIN_POOL_SIZE), newMax);
-        } else {
-            throw new UnsupportedOperationException("reconfiguring "+key+" unsupported for "+this);
-        }
-    }
-
-    @Override
-    public void suspend() {
-        super.suspend();
-        // TODO unsubscribe from everything? And resubscribe on resume?
-        if (executor != null) executor.shutdownNow();
-    }
-    
-    @Override
-    public void resume() {
-        super.resume();
-        executor = Executors.newSingleThreadScheduledExecutor(newThreadFactory());
-    }
-    
-    @Override
-    public void setEntity(EntityLocal entity) {
-        if (!config().getRaw(RESIZE_OPERATOR).isPresentAndNonNull()) {
-            Preconditions.checkArgument(entity instanceof Resizable, "Provided entity "+entity+" must be an instance of Resizable, because no custom-resizer operator supplied");
-        }
-        super.setEntity(entity);
-        this.poolEntity = entity;
-        
-        if (getMetric() != null) {
-            Entity entityToSubscribeTo = (getEntityWithMetric() != null) ? getEntityWithMetric() : entity;
-            subscribe(entityToSubscribeTo, getMetric(), metricEventHandler);
-        }
-        subscribe(poolEntity, getPoolColdSensor(), utilizationEventHandler);
-        subscribe(poolEntity, getPoolHotSensor(), utilizationEventHandler);
-        subscribe(poolEntity, getPoolOkSensor(), utilizationEventHandler);
-    }
-    
-    private ThreadFactory newThreadFactory() {
-        return new ThreadFactoryBuilder()
-                .setNameFormat("brooklyn-autoscalerpolicy-%d")
-                .build();
-    }
-
-    /**
-     * Forces an immediate resize (without waiting for stabilization etc) if the current size is 
-     * not within the min and max limits. We schedule this so that all resize operations are done
-     * by the same thread.
-     */
-    private void onPoolSizeLimitsChanged(final int min, final int max) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} checking pool size on limits changed for {} (between {} and {})", new Object[] {this, poolEntity, min, max});
-        
-        if (isRunning() && isEntityUp()) {
-            executor.submit(new Runnable() {
-                @Override public void run() {
-                    try {
-                        int currentSize = getCurrentSizeOperator().apply(entity);
-                        int desiredSize = Math.min(max, Math.max(min, currentSize));
-
-                        if (currentSize != desiredSize) {
-                            if (LOG.isInfoEnabled()) LOG.info("{} resizing pool {} immediateley from {} to {} (due to new pool size limits)", new Object[] {this, poolEntity, currentSize, desiredSize});
-                            getResizeOperator().resize(poolEntity, desiredSize);
-                        }
-                        
-                    } catch (Exception e) {
-                        if (isRunning()) {
-                            LOG.error("Error resizing: "+e, e);
-                        } else {
-                            if (LOG.isDebugEnabled()) LOG.debug("Error resizing, but no longer running: "+e, e);
-                        }
-                    } catch (Throwable t) {
-                        LOG.error("Error resizing: "+t, t);
-                        throw Throwables.propagate(t);
-                    }
-                }});
-        }
-    }
-    
-    private enum ScalingType { HOT, COLD }
-    private static class ScalingData {
-        ScalingType scalingMode;
-        int currentSize;
-        double currentMetricValue;
-        Double metricUpperBound;
-        Double metricLowerBound;
-        
-        public double getCurrentTotalActivity() {
-            return currentMetricValue * currentSize;
-        }
-        
-        public boolean isHot() {
-            return ((scalingMode==null || scalingMode==ScalingType.HOT) && isValid(metricUpperBound) && currentMetricValue > metricUpperBound);
-        }
-        public boolean isCold() {
-            return ((scalingMode==null || scalingMode==ScalingType.COLD) && isValid(metricLowerBound) && currentMetricValue < metricLowerBound);
-        }
-        private boolean isValid(Double bound) {
-            return (bound!=null && bound>0);
-        }
-    }
-
-    private void onMetricChanged(Number val) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-metric for {}: {}", new Object[] {this, poolEntity, val});
-
-        if (val==null) {
-            // occurs e.g. if using an aggregating enricher who returns null when empty, the sensor has gone away
-            if (LOG.isTraceEnabled()) LOG.trace("{} not resizing pool {}, inbound metric is null", new Object[] {this, poolEntity});
-            return;
-        }
-        
-        ScalingData data = new ScalingData();
-        data.currentMetricValue = val.doubleValue();
-        data.currentSize = getCurrentSizeOperator().apply(entity);
-        data.metricUpperBound = getMetricUpperBound().doubleValue();
-        data.metricLowerBound = getMetricLowerBound().doubleValue();
-        
-        analyze(data, "pool");
-    }
-    
-    private void onPoolCold(Map<String, ?> properties) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-cold for {}: {}", new Object[] {this, poolEntity, properties});
-        analyzeOnHotOrColdSensor(ScalingType.COLD, "cold pool", properties);
-    }
-    
-    private void onPoolHot(Map<String, ?> properties) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-hot for {}: {}", new Object[] {this, poolEntity, properties});
-        analyzeOnHotOrColdSensor(ScalingType.HOT, "hot pool", properties);
-    }
-    
-    private void analyzeOnHotOrColdSensor(ScalingType scalingMode, String description, Map<String, ?> properties) {
-        ScalingData data = new ScalingData();
-        data.scalingMode = scalingMode;
-        data.currentMetricValue = (Double) properties.get(POOL_CURRENT_WORKRATE_KEY);
-        data.currentSize = (Integer) properties.get(POOL_CURRENT_SIZE_KEY);
-        data.metricUpperBound = (Double) properties.get(POOL_HIGH_THRESHOLD_KEY);
-        data.metricLowerBound = (Double) properties.get(POOL_LOW_THRESHOLD_KEY);
-        
-        analyze(data, description);   
-    }
-    
-    private void analyze(ScalingData data, String description) {
-        int desiredSizeUnconstrained;
-        
-        /* We always scale out (modulo stabilization delay) if:
-         *   currentTotalActivity > currentSize*metricUpperBound
-         * With newDesiredSize the smallest n such that   n*metricUpperBound >= currentTotalActivity
-         * ie  n >= currentTotalActiviy/metricUpperBound, thus n := Math.ceil(currentTotalActivity/metricUpperBound)
-         * 
-         * Else consider scale back if:
-         *   currentTotalActivity < currentSize*metricLowerBound
-         * With newDesiredSize normally the largest n such that:  
-         *   n*metricLowerBound <= currentTotalActivity
-         * BUT with an absolute requirement which trumps the above computation
-         * that the newDesiredSize doesn't cause immediate scale out:
-         *   n*metricUpperBound >= currentTotalActivity
-         * thus n := Math.max ( floor(currentTotalActiviy/metricLowerBound), ceil(currentTotal/metricUpperBound) )
-         */
-        if (data.isHot()) {
-            // scale out
-            desiredSizeUnconstrained = (int)Math.ceil(data.getCurrentTotalActivity() / data.metricUpperBound);
-            data.scalingMode = ScalingType.HOT;
-            
-        } else if (data.isCold()) {
-            // scale back
-            desiredSizeUnconstrained = (int)Math.floor(data.getCurrentTotalActivity() / data.metricLowerBound);
-            data.scalingMode = ScalingType.COLD;
-            
-        } else {
-            if (LOG.isTraceEnabled()) LOG.trace("{} not resizing pool {} from {} ({} within range {}..{})", new Object[] {this, poolEntity, data.currentSize, data.currentMetricValue, data.metricLowerBound, data.metricUpperBound});
-            abortResize(data.currentSize);
-            return; // within the healthy range; no-op
-        }
-        
-        if (LOG.isTraceEnabled()) LOG.debug("{} detected unconstrained desired size {}", new Object[] {this, desiredSizeUnconstrained});
-        int desiredSize = applyMinMaxConstraints(desiredSizeUnconstrained);
-
-        if ((data.scalingMode==ScalingType.COLD) && (desiredSize < data.currentSize)) {
-
-            int delta = data.currentSize - desiredSize;
-            int scaleIncrement = getResizeDownIterationIncrement();
-            int scaleMax = getResizeDownIterationMax();
-            if (delta>scaleMax) {
-                delta=scaleMax;
-            } else if (delta % scaleIncrement != 0) {
-                // keep scaling to the increment
-                delta += scaleIncrement - (delta % scaleIncrement);
-            }
-            desiredSize = data.currentSize - delta;
-            
-            if (data.metricUpperBound!=null) {
-                // if upper bound supplied, check that this desired scale-back size 
-                // is not going to cause scale-out on next run; i.e. anti-thrashing
-                while (desiredSize < data.currentSize && data.getCurrentTotalActivity() > data.metricUpperBound * desiredSize) {
-                    if (LOG.isTraceEnabled()) LOG.trace("{} when resizing back pool {} from {}, tweaking from {} to prevent thrashing", new Object[] {this, poolEntity, data.currentSize, desiredSize });
-                    desiredSize += scaleIncrement;
-                }
-            }
-            desiredSize = applyMinMaxConstraints(desiredSize);
-            if (desiredSize >= data.currentSize) data.scalingMode = null;
-            
-        } else if ((data.scalingMode==ScalingType.HOT) && (desiredSize > data.currentSize)) {
-
-            int delta = desiredSize - data.currentSize;
-            int scaleIncrement = getResizeUpIterationIncrement();
-            int scaleMax = getResizeUpIterationMax();
-            if (delta>scaleMax) {
-                delta=scaleMax;
-            } else if (delta % scaleIncrement != 0) {
-                // keep scaling to the increment
-                delta += scaleIncrement - (delta % scaleIncrement);
-            }
-            desiredSize = data.currentSize + delta;
-            desiredSize = applyMinMaxConstraints(desiredSize);
-            if (desiredSize <= data.currentSize) data.scalingMode = null;
-
-        } else {
-            data.scalingMode = null;
-        }
-    
-        if (data.scalingMode!=null) {
-            if (LOG.isDebugEnabled()) LOG.debug("{} provisionally resizing {} {} from {} to {} ({} < {}; ideal size {})", new Object[] {this, description, poolEntity, data.currentSize, desiredSize, data.currentMetricValue, data.metricLowerBound, desiredSizeUnconstrained});
-            scheduleResize(desiredSize);
-        } else {
-            if (LOG.isTraceEnabled()) LOG.trace("{} not resizing {} {} from {} to {}, {} out of healthy range {}..{} but unconstrained size {} blocked by bounds/check", new Object[] {this, description, poolEntity, data.currentSize, desiredSize, data.currentMetricValue, data.metricLowerBound, data.metricUpperBound, desiredSizeUnconstrained});
-            abortResize(data.currentSize);
-            // but add to the unbounded record for future consideration
-        }
-        
-        onNewUnboundedPoolSize(desiredSizeUnconstrained);
-    }
-
-    private int applyMinMaxConstraints(int desiredSize) {
-        desiredSize = Math.max(getMinPoolSize(), desiredSize);
-        desiredSize = Math.min(getMaxPoolSize(), desiredSize);
-        return desiredSize;
-    }
-
-    private void onPoolOk(Map<String, ?> properties) {
-        if (LOG.isTraceEnabled()) LOG.trace("{} recording pool-ok for {}: {}", new Object[] {this, poolEntity, properties});
-        
-        int poolCurrentSize = (Integer) properties.get(POOL_CURRENT_SIZE_KEY);
-        
-        if (LOG.isTraceEnabled()) LOG.trace("{} not resizing ok pool {} from {}", new Object[] {this, poolEntity, poolCurrentSize});
-        abortResize(poolCurrentSize);
-    }
-
-    /**
-     * Schedules a resize, if there is not already a resize operation queued up. When that resize
-     * executes, it will resize to whatever the latest value is to be (rather than what it was told
-     * to do at the point the job was queued).
-     */
-    private void scheduleResize(final int newSize) {
-        recentDesiredResizes.add(newSize);
-        
-        scheduleResize();
-    }
-
-    /**
-     * If a listener is registered to be notified of the max-pool-size cap being reached, then record
-     * what our unbounded size would be and schedule a check to see if this unbounded size is sustained.
-     * 
-     * Piggy-backs off the existing scheduleResize execution, which now also checks if the listener
-     * needs to be called.
-     */
-    private void onNewUnboundedPoolSize(final int val) {
-        if (getMaxSizeReachedSensor() != null) {
-            recentUnboundedResizes.add(val);
-            scheduleResize();
-        }
-    }
-    
-    private void abortResize(final int currentSize) {
-        recentDesiredResizes.add(currentSize);
-        recentUnboundedResizes.add(currentSize);
-    }
-
-    private boolean isEntityUp() {
-        if (entity == null) {
-            return false;
-        } else if (entity.getEntityType().getSensors().contains(Startable.SERVICE_UP)) {
-            return Boolean.TRUE.equals(entity.getAttribute(Startable.SERVICE_UP));
-        } else {
-            return true;
-        }
-    }
-
-    private void scheduleResize() {
-        // TODO Make scale-out calls concurrent, rather than waiting for first resize to entirely 
-        // finish. On ec2 for example, this can cause us to grow very slowly if first request is for
-        // just one new VM to be provisioned.
-        
-        if (isRunning() && isEntityUp() && executorQueued.compareAndSet(false, true)) {
-            long now = System.currentTimeMillis();
-            long delay = Math.max(0, (executorTime + getMinPeriodBetweenExecs().toMilliseconds()) - now);
-            if (LOG.isTraceEnabled()) LOG.trace("{} scheduling resize in {}ms", this, delay);
-            
-            executor.schedule(new Runnable() {
-                @Override public void run() {
-                    try {
-                        executorTime = System.currentTimeMillis();
-                        executorQueued.set(false);
-
-                        resizeNow();
-                        notifyMaxReachedIfRequiredNow();
-                        
-                    } catch (Exception e) {
-                        if (isRunning()) {
-                            LOG.error("Error resizing: "+e, e);
-                        } else {
-                            if (LOG.isDebugEnabled()) LOG.debug("Error resizing, but no longer running: "+e, e);
-                        }
-                    } catch (Throwable t) {
-                        LOG.error("Error resizing: "+t, t);
-                        throw Throwables.propagate(t);
-                    }
-                }},
-                delay,
-                TimeUnit.MILLISECONDS);
-        }
-    }
-
-    /**
-     * Looks at the values for "unbounded pool size" (i.e. if we ignore caps of minSize and maxSize) to report what
-     * those values have been within a time window. The time window used is the "maxReachedNotificationDelay",
-     * which determines how many milliseconds after being consistently above the max-size will it take before
-     * we emit the sensor event (if any).
-     */
-    private void notifyMaxReachedIfRequiredNow() {
-        BasicNotificationSensor<? super MaxPoolSizeReachedEvent> maxSizeReachedSensor = getMaxSizeReachedSensor();
-        if (maxSizeReachedSensor == null) {
-            return;
-        }
-        
-        WindowSummary valsSummary = recentUnboundedResizes.summarizeWindow(getMaxReachedNotificationDelay());
-        long timeWindowSize = getMaxReachedNotificationDelay().toMilliseconds();
-        long currentPoolSize = getCurrentSizeOperator().apply(poolEntity);
-        int maxAllowedPoolSize = getMaxPoolSize();
-        long unboundedSustainedMaxPoolSize = valsSummary.min; // The sustained maximum (i.e. the smallest it's dropped down to)
-        long unboundedCurrentPoolSize = valsSummary.latest;
-        
-        if (maxReachedLastNotifiedTime > 0) {
-            // already notified the listener; don't do it again
-            // TODO Could have max period for notifications, or a step increment to warn when exceeded by ever bigger amounts
-            
-        } else if (unboundedSustainedMaxPoolSize > maxAllowedPoolSize) {
-            // We have consistently wanted to be bigger than the max allowed; tell the listener
-            if (LOG.isDebugEnabled()) LOG.debug("{} notifying listener of max pool size reached; current {}, max {}, unbounded current {}, unbounded max {}", 
-                    new Object[] {this, currentPoolSize, maxAllowedPoolSize, unboundedCurrentPoolSize, unboundedSustainedMaxPoolSize});
-            
-            maxReachedLastNotifiedTime = System.currentTimeMillis();
-            MaxPoolSizeReachedEvent event = MaxPoolSizeReachedEvent.builder()
-                    .currentPoolSize(currentPoolSize)
-                    .maxAllowed(maxAllowedPoolSize)
-                    .currentUnbounded(unboundedCurrentPoolSize)
-                    .maxUnbounded(unboundedSustainedMaxPoolSize)
-                    .timeWindow(timeWindowSize)
-                    .build();
-            entity.emit(maxSizeReachedSensor, event);
-            
-        } else if (valsSummary.max > maxAllowedPoolSize) {
-            // We temporarily wanted to be bigger than the max allowed; check back later to see if consistent
-            // TODO Could check if there has been anything bigger than "min" since min happened (would be more efficient)
-            if (LOG.isTraceEnabled()) LOG.trace("{} re-scheduling max-reached check for {}, as unbounded size not stable (min {}, max {}, latest {})", 
-                    new Object[] {this, poolEntity, valsSummary.min, valsSummary.max, valsSummary.latest});
-            scheduleResize();
-            
-        } else {
-            // nothing to write home about; continually below maxAllowed
-        }
-    }
-
-    private void resizeNow() {
-        long currentPoolSize = getCurrentSizeOperator().apply(poolEntity);
-        CalculatedDesiredPoolSize calculatedDesiredPoolSize = calculateDesiredPoolSize(currentPoolSize);
-        final long desiredPoolSize = calculatedDesiredPoolSize.size;
-        boolean stable = calculatedDesiredPoolSize.stable;
-        
-        if (!stable) {
-            // the desired size fluctuations are not stable; ensure we check again later (due to time-window)
-            // even if no additional events have been received
-            // (note we continue now with as "good" a resize as we can given the instability)
-            if (LOG.isTraceEnabled()) LOG.trace("{} re-scheduling resize check for {}, as desired size not stable (current {}, desired {}); continuing with resize...", 
-                    new Object[] {this, poolEntity, currentPoolSize, desiredPoolSize});
-            scheduleResize();
-        }
-        if (currentPoolSize == desiredPoolSize) {
-            if (LOG.isTraceEnabled()) LOG.trace("{} not resizing pool {} from {} to {}", 
-                    new Object[] {this, poolEntity, currentPoolSize, desiredPoolSize});
-            return;
-        }
-        
-        if (LOG.isDebugEnabled()) LOG.debug("{} requesting resize to {}; current {}, min {}, max {}", 
-                new Object[] {this, desiredPoolSize, currentPoolSize, getMinPoolSize(), getMaxPoolSize()});
-        
-        Entities.submit(entity, Tasks.<Void>builder().name("Auto-scaler")
-            .description("Auto-scaler recommending resize from "+currentPoolSize+" to "+desiredPoolSize)
-            .tag(BrooklynTaskTags.NON_TRANSIENT_TASK_TAG)
-            .body(new Callable<Void>() {
-                @Override
-                public Void call() throws Exception {
-                    // TODO Should we use int throughout, rather than casting here?
-                    getResizeOperator().resize(poolEntity, (int) desiredPoolSize);
-                    return null;
-                }
-            }).build())
-            .blockUntilEnded();
-    }
-    
-    /**
-     * Complicated logic for stabilization-delay...
-     * Only grow if we have consistently been asked to grow for the resizeUpStabilizationDelay period;
-     * Only shrink if we have consistently been asked to shrink for the resizeDownStabilizationDelay period.
-     * 
-     * @return tuple of desired pool size, and whether this is "stable" (i.e. if we receive no more events 
-     *         will this continue to be the desired pool size)
-     */
-    private CalculatedDesiredPoolSize calculateDesiredPoolSize(long currentPoolSize) {
-        long now = System.currentTimeMillis();
-        WindowSummary downsizeSummary = recentDesiredResizes.summarizeWindow(getResizeDownStabilizationDelay());
-        WindowSummary upsizeSummary = recentDesiredResizes.summarizeWindow(getResizeUpStabilizationDelay());
-        
-        // this is the _sustained_ growth value; the smallest size that has been requested in the "stable-for-growing" period
-        long maxDesiredPoolSize = upsizeSummary.min;
-        boolean stableForGrowing = upsizeSummary.stableForGrowth;
-        
-        // this is the _sustained_ shrink value; largest size that has been requested in the "stable-for-shrinking" period:
-        long minDesiredPoolSize = downsizeSummary.max;
-        boolean stableForShrinking = downsizeSummary.stableForShrinking;
-        
-        // (it is a logical consequence of the above that minDesired >= maxDesired -- this is correct, if confusing:
-        // think of minDesired as the minimum size we are allowed to resize to, and similarly for maxDesired; 
-        // if min > max we can scale to max if current < max, or scale to min if current > min)
-
-        long desiredPoolSize;
-        
-        boolean stable;
-        
-        if (currentPoolSize < maxDesiredPoolSize) {
-            // we have valid request to grow 
-            // (we'll never have a valid request to grow and a valid to shrink simultaneously, btw)
-            desiredPoolSize = maxDesiredPoolSize;
-            stable = stableForGrowing;
-        } else if (currentPoolSize > minDesiredPoolSize) {
-            // we have valid request to shrink
-            desiredPoolSize = minDesiredPoolSize;
-            stable = stableForShrinking;
-        } else {
-            desiredPoolSize = currentPoolSize;
-            stable = stableForGrowing && stableForShrinking;
-        }
-
-        if (LOG.isTraceEnabled()) LOG.trace("{} calculated desired pool size: from {} to {}; minDesired {}, maxDesired {}; " +
-                "stable {}; now {}; downsizeHistory {}; upsizeHistory {}", 
-                new Object[] {this, currentPoolSize, desiredPoolSize, minDesiredPoolSize, maxDesiredPoolSize, stable, now, downsizeSummary, upsizeSummary});
-        
-        return new CalculatedDesiredPoolSize(desiredPoolSize, stable);
-    }
-    
-    private static class CalculatedDesiredPoolSize {
-        final long size;
-        final boolean stable;
-        
-        CalculatedDesiredPoolSize(long size, boolean stable) {
-            this.size = size;
-            this.stable = stable;
-        }
-    }
-    
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + (groovyTruth(name) ? "("+name+")" : "");
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/autoscaling/MaxPoolSizeReachedEvent.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/autoscaling/MaxPoolSizeReachedEvent.java b/policy/src/main/java/brooklyn/policy/autoscaling/MaxPoolSizeReachedEvent.java
deleted file mode 100644
index c0a6c7d..0000000
--- a/policy/src/main/java/brooklyn/policy/autoscaling/MaxPoolSizeReachedEvent.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.autoscaling;
-
-import java.io.Serializable;
-
-import com.google.common.base.Objects;
-
-public class MaxPoolSizeReachedEvent implements Serializable {
-    private static final long serialVersionUID = 1602627701360505190L;
-
-    public static Builder builder() {
-        return new Builder();
-    }
-
-    public static class Builder {
-        protected long maxAllowed;
-        protected long currentPoolSize;
-        protected long currentUnbounded;
-        protected long maxUnbounded;
-        protected long timeWindow;
-        
-        public Builder maxAllowed(long val) {
-            this.maxAllowed = val; return this;
-        }
-
-        public Builder currentPoolSize(long val) {
-            this.currentPoolSize = val; return this;
-        }
-
-        public Builder currentUnbounded(long val) {
-            this.currentUnbounded = val; return this;
-        }
-
-        public Builder maxUnbounded(long val) {
-            this.maxUnbounded = val; return this;
-        }
-
-        public Builder timeWindow(long val) {
-            this.timeWindow = val; return this;
-        }
-        public MaxPoolSizeReachedEvent build() {
-            return new MaxPoolSizeReachedEvent(this);
-        }
-    }
-    
-    private final long maxAllowed;
-    private final long currentPoolSize;
-    private final long currentUnbounded;
-    private final long maxUnbounded;
-    private final long timeWindow;
-    
-    protected MaxPoolSizeReachedEvent(Builder builder) {
-        maxAllowed = builder.maxAllowed;
-        currentPoolSize = builder.currentPoolSize;
-        currentUnbounded = builder.currentUnbounded;
-        maxUnbounded = builder.maxUnbounded;
-        timeWindow = builder.timeWindow;
-    }
-    
-    public long getMaxAllowed() {
-        return maxAllowed;
-    }
-    
-    public long getCurrentPoolSize() {
-        return currentPoolSize;
-    }
-    
-    public long getCurrentUnbounded() {
-        return currentUnbounded;
-    }
-    
-    public long getMaxUnbounded() {
-        return maxUnbounded;
-    }
-    
-    public long getTimeWindow() {
-        return timeWindow;
-    }
-    
-    @Override
-    public String toString() {
-        return Objects.toStringHelper(this).add("maxAllowed", maxAllowed).add("currentPoolSize", currentPoolSize)
-                .add("currentUnbounded", currentUnbounded).add("maxUnbounded", maxUnbounded)
-                .add("timeWindow", timeWindow).toString();
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/d30ff597/policy/src/main/java/brooklyn/policy/autoscaling/ResizeOperator.java
----------------------------------------------------------------------
diff --git a/policy/src/main/java/brooklyn/policy/autoscaling/ResizeOperator.java b/policy/src/main/java/brooklyn/policy/autoscaling/ResizeOperator.java
deleted file mode 100644
index 80e1293..0000000
--- a/policy/src/main/java/brooklyn/policy/autoscaling/ResizeOperator.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.policy.autoscaling;
-
-import org.apache.brooklyn.api.entity.Entity;
-
-public interface ResizeOperator {
-
-    /**
-     * Resizes the given entity to the desired size, if possible.
-     * 
-     * @return the new size of the entity
-     */
-    public Integer resize(Entity entity, Integer desiredSize);
-}