http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/main/java/brooklyn/entity/basic/ServiceStateLogic.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/ServiceStateLogic.java 
b/core/src/main/java/brooklyn/entity/basic/ServiceStateLogic.java
new file mode 100644
index 0000000..de4f1ad
--- /dev/null
+++ b/core/src/main/java/brooklyn/entity/basic/ServiceStateLogic.java
@@ -0,0 +1,440 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package brooklyn.entity.basic;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.enricher.Enrichers;
+import brooklyn.enricher.basic.AbstractEnricher;
+import brooklyn.enricher.basic.AbstractMultipleSensorAggregator;
+import brooklyn.enricher.basic.UpdatingMap;
+import brooklyn.entity.Effector;
+import brooklyn.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.Lifecycle.Transition;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.Sensor;
+import brooklyn.event.SensorEvent;
+import brooklyn.event.SensorEventListener;
+import brooklyn.policy.Enricher;
+import brooklyn.policy.EnricherSpec;
+import brooklyn.policy.EnricherSpec.ExtensibleEnricherSpec;
+import brooklyn.util.collections.CollectionFunctionals;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.guava.Functionals;
+import brooklyn.util.text.Strings;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+/** Logic, sensors and enrichers, and conveniences, for computing service 
status */ 
+public class ServiceStateLogic {
+
+    public static final AttributeSensor<Boolean> SERVICE_UP = 
Attributes.SERVICE_UP;
+    public static final AttributeSensor<Map<String,Object>> 
SERVICE_NOT_UP_INDICATORS = Attributes.SERVICE_NOT_UP_INDICATORS;
+    
+    public static final AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = 
Attributes.SERVICE_STATE_ACTUAL;
+    public static final AttributeSensor<Lifecycle.Transition> 
SERVICE_STATE_EXPECTED = Attributes.SERVICE_STATE_EXPECTED;
+    public static final AttributeSensor<Map<String,Object>> SERVICE_PROBLEMS = 
Attributes.SERVICE_PROBLEMS;
+
+    /** static only; not for instantiation */
+    private ServiceStateLogic() {}
+
+    @SuppressWarnings("unchecked")
+    public static <TKey,TVal> void clearMapSensorEntry(EntityLocal entity, 
AttributeSensor<Map<TKey,TVal>> sensor, TKey key) {
+        updateMapSensorEntry(entity, sensor, key, (TVal)Entities.REMOVE);
+    }
+
+    /** update the given key in the given map sensor */
+    public static <TKey,TVal> void updateMapSensorEntry(EntityLocal entity, 
AttributeSensor<Map<TKey,TVal>> sensor, TKey key, TVal v) {
+        Map<TKey, TVal> map = entity.getAttribute(sensor);
+
+        // TODO synchronize
+        
+        boolean created = (map==null);
+        if (created) map = MutableMap.of();
+                
+        boolean changed;
+        if (v == Entities.REMOVE) {
+            changed = map.containsKey(key);
+            if (changed)
+                map.remove(key);
+        } else {
+            TVal oldV = map.get(key);
+            if (oldV==null)
+                changed = (v!=null || !map.containsKey(key));
+            else
+                changed = !oldV.equals(v);
+            if (changed)
+                map.put(key, (TVal)v);
+        }
+        if (changed || created)
+            entity.setAttribute(sensor, map);
+    }
+    
+    public static void setExpectedState(Entity entity, Lifecycle state) {
+        
((EntityInternal)entity).setAttribute(Attributes.SERVICE_STATE_EXPECTED, new 
Lifecycle.Transition(state, new Date()));
+    }
+    public static Lifecycle getExpectedState(Entity entity) {
+        Transition expected = 
entity.getAttribute(Attributes.SERVICE_STATE_EXPECTED);
+        if (expected==null) return null;
+        return expected.getState();
+    }
+    public static boolean isExpectedState(Entity entity, Lifecycle state) {
+        return getExpectedState(entity)==state;
+    }
+    
+    public static class ServiceNotUpLogic {
+        /** static only; not for instantiation */
+        private ServiceNotUpLogic() {}
+        
+        @SuppressWarnings({ "unchecked", "rawtypes" })
+        public static final EnricherSpec<?> 
newEnricherForServiceUpIfNoNotUpIndicators() {
+            return Enrichers.builder()
+                
.transforming(SERVICE_NOT_UP_INDICATORS).publishing(Attributes.SERVICE_UP)
+                .computing( /* cast hacks to support removing */ (Function)
+                    Functionals.<Map<String,?>>
+                        
ifNotEquals(null).<Object>apply(Functions.forPredicate(CollectionFunctionals.<String>mapSizeEquals(0)))
+                        .defaultValue(Entities.REMOVE) )
+                .uniqueTag("service.isUp if no service.notUp.indicators")
+                .build();
+        }
+        
+        /** puts the given value into the {@link 
Attributes#SERVICE_NOT_UP_INDICATORS} map as if the 
+         * {@link UpdatingMap} enricher for the given sensor reported */
+        public static void updateNotUpIndicator(EntityLocal entity, Sensor<?> 
sensor, Object value) {
+            updateMapSensorEntry(entity, Attributes.SERVICE_NOT_UP_INDICATORS, 
sensor.getName(), value);
+        }
+        /** clears any entry for the given sensor in the {@link 
Attributes#SERVICE_NOT_UP_INDICATORS} map */
+        public static void clearNotUpIndicator(EntityLocal entity, Sensor<?> 
sensor) {
+            clearMapSensorEntry(entity, Attributes.SERVICE_NOT_UP_INDICATORS, 
sensor.getName());
+        }
+
+        public static void 
updateNotUpIndicatorRequiringNonEmptyList(EntityLocal entity, AttributeSensor<? 
extends Collection<?>> collectionSensor) {
+            Collection<?> nodes = entity.getAttribute(collectionSensor);
+            if (nodes==null || nodes.isEmpty()) 
ServiceNotUpLogic.updateNotUpIndicator(entity, collectionSensor, "Should have 
at least one entry");
+            else ServiceNotUpLogic.clearNotUpIndicator(entity, 
collectionSensor);
+        }
+        public static void 
updateNotUpIndicatorRequiringNonEmptyMap(EntityLocal entity, AttributeSensor<? 
extends Map<?,?>> mapSensor) {
+            Map<?, ?> nodes = entity.getAttribute(mapSensor);
+            if (nodes==null || nodes.isEmpty()) 
ServiceNotUpLogic.updateNotUpIndicator(entity, mapSensor, "Should have at least 
one entry");
+            else ServiceNotUpLogic.clearNotUpIndicator(entity, mapSensor);
+        }
+        
+    }
+    
+    /** Enricher which sets {@link Attributes#SERVICE_STATE_ACTUAL} on changes 
to 
+     * {@link Attributes#SERVICE_STATE_EXPECTED}, {@link 
Attributes#SERVICE_PROBLEMS}, and {@link Attributes#SERVICE_UP}
+     * <p>
+     * The default implementation uses {@link 
#computeActualStateWhenExpectedRunning(Map, Boolean)} if the last expected 
transition
+     * was to {@link Lifecycle#RUNNING} and 
+     * {@link #computeActualStateWhenNotExpectedRunning(Map, Boolean, 
brooklyn.entity.basic.Lifecycle.Transition)} otherwise.
+     * If these methods return null, the {@link 
Attributes#SERVICE_STATE_ACTUAL} sensor will be cleared (removed).
+     * Either of these methods can be overridden for custom logic, and that 
custom enricher can be created using 
+     * {@link ServiceStateLogic#newEnricherForServiceState(Class)} and added 
to an entity.
+     */
+    public static class ComputeServiceState extends AbstractEnricher 
implements SensorEventListener<Object> {
+        public void setEntity(EntityLocal entity) {
+            super.setEntity(entity);
+            if (suppressDuplicates==null) {
+                // only publish on changes, unless it is configured otherwise
+                suppressDuplicates = Boolean.TRUE;
+            }
+            
+            subscribe(entity, SERVICE_PROBLEMS, this);
+            subscribe(entity, SERVICE_UP, this);
+            subscribe(entity, SERVICE_STATE_EXPECTED, this);
+            onEvent(null);
+        }
+
+        @Override
+        public void onEvent(@Nullable SensorEvent<Object> event) {
+            Preconditions.checkNotNull(entity, "Cannot handle subscriptions or 
compute state until associated with an entity");
+            
+            Map<String, Object> serviceProblems = 
entity.getAttribute(SERVICE_PROBLEMS);
+            Boolean serviceUp = entity.getAttribute(SERVICE_UP);
+            Lifecycle.Transition serviceExpected = 
entity.getAttribute(SERVICE_STATE_EXPECTED);
+            
+            if (serviceExpected!=null && 
serviceExpected.getState()==Lifecycle.RUNNING) {
+                setActualState( 
computeActualStateWhenExpectedRunning(serviceProblems, serviceUp) );
+            } else {
+                setActualState( 
computeActualStateWhenNotExpectedRunning(serviceProblems, serviceUp, 
serviceExpected) );
+            }
+        }
+
+        protected Lifecycle computeActualStateWhenExpectedRunning(Map<String, 
Object> problems, Boolean serviceUp) {
+            if (Boolean.TRUE.equals(serviceUp) && (problems==null || 
problems.isEmpty())) {
+                return Lifecycle.RUNNING;
+            } else {
+                return Lifecycle.ON_FIRE;
+            }
+        }
+        
+        protected Lifecycle 
computeActualStateWhenNotExpectedRunning(Map<String, Object> problems, Boolean 
up, Lifecycle.Transition stateTransition) {
+            if (stateTransition!=null) {
+                return stateTransition.getState();
+            } else if (problems!=null && !problems.isEmpty()) {
+                return Lifecycle.ON_FIRE;
+            } else {
+                return (up==null ? null : up ? Lifecycle.RUNNING : 
Lifecycle.STOPPED);
+            }
+        }
+
+        protected void setActualState(@Nullable Lifecycle state) {
+            emit(SERVICE_STATE_ACTUAL, (state==null ? Entities.REMOVE : 
state));
+        }
+    }
+    
+    public static final EnricherSpec<?> 
newEnricherForServiceStateFromProblemsAndUp() {
+        return newEnricherForServiceState(ComputeServiceState.class);
+    }
+    public static final EnricherSpec<?> newEnricherForServiceState(Class<? 
extends Enricher> type) {
+        return EnricherSpec.create(type).uniqueTag("service.state.actual from 
service.state.expected and service.problems");
+    }
+    
+    public static class ServiceProblemsLogic {
+        /** static only; not for instantiation */
+        private ServiceProblemsLogic() {}
+        
+        /** puts the given value into the {@link Attributes#SERVICE_PROBLEMS} 
map as if the 
+         * {@link UpdatingMap} enricher for the given sensor reported this 
value */
+        public static void updateProblemsIndicator(EntityLocal entity, 
Sensor<?> sensor, Object value) {
+            updateMapSensorEntry(entity, Attributes.SERVICE_PROBLEMS, 
sensor.getName(), value);
+        }
+        /** clears any entry for the given sensor in the {@link 
Attributes#SERVICE_PROBLEMS} map */
+        public static void clearProblemsIndicator(EntityLocal entity, 
Sensor<?> sensor) {
+            clearMapSensorEntry(entity, Attributes.SERVICE_PROBLEMS, 
sensor.getName());
+        }
+        /** as {@link #updateProblemsIndicator(EntityLocal, Sensor, Object)} */
+        public static void updateProblemsIndicator(EntityLocal entity, 
Effector<?> eff, Object value) {
+            updateMapSensorEntry(entity, Attributes.SERVICE_PROBLEMS, 
eff.getName(), value);
+        }
+        /** as {@link #clearProblemsIndicator(EntityLocal, Sensor)} */
+        public static void clearProblemsIndicator(EntityLocal entity, 
Effector<?> eff) {
+            clearMapSensorEntry(entity, Attributes.SERVICE_PROBLEMS, 
eff.getName());
+        }
+    }
+    
+    public static class ComputeServiceIndicatorsFromChildrenAndMembers extends 
AbstractMultipleSensorAggregator<Void> implements SensorEventListener<Object> {
+        /** standard unique tag identifying instances of this enricher at 
runtime, also used for the map sensor if no unique tag specified */
+        public final static String IDENTIFIER_DEFAULT = 
"service-lifecycle-indicators-from-children-and-members";
+        
+        /** as {@link #IDENTIFIER_LIFECYCLE}, but when a second distinct 
instance is responsible for computing service up */
+        public final static String IDENTIFIER_UP = 
"service-not-up-indicators-from-children-and-members";
+
+        public static final ConfigKey<QuorumCheck> UP_QUORUM_CHECK = 
ConfigKeys.newConfigKey(QuorumCheck.class, 
"enricher.service_state.children_and_members.quorum.up", 
+            "Logic for checking whether this service is up, based on children 
and/or members, defaulting to allowing none but if there are any requiring at 
least one to be up", QuorumCheck.QuorumChecks.atLeastOneUnlessEmpty());
+        public static final ConfigKey<QuorumCheck> RUNNING_QUORUM_CHECK = 
ConfigKeys.newConfigKey(QuorumCheck.class, 
"enricher.service_state.children_and_members.quorum.running", 
+            "Logic for checking whether this service is healthy, based on 
children and/or members running, defaulting to requiring none to be ON-FIRE", 
QuorumCheck.QuorumChecks.all());
+        public static final ConfigKey<Boolean> DERIVE_SERVICE_NOT_UP = 
ConfigKeys.newBooleanConfigKey("enricher.service_state.children_and_members.service_up.publish",
 "Whether to derive a service-not-up indicator from children", true);
+        public static final ConfigKey<Boolean> DERIVE_SERVICE_PROBLEMS = 
ConfigKeys.newBooleanConfigKey("enricher.service_state.children_and_members.service_problems.publish",
 "Whether to derive a service-problem indicator from children", true);
+
+        protected String getKeyForMapSensor() {
+            return Preconditions.checkNotNull(super.getUniqueTag());
+        }
+
+        @Override
+        protected void setEntityLoadingConfig() {
+            fromChildren = true;
+            fromMembers = true;
+            // above sets default
+            super.setEntityLoadingConfig();
+            if (fromMembers && (!(entity instanceof Group))) {
+                if (fromChildren) fromMembers=false;
+                else throw new IllegalStateException("Cannot monitor only 
members for non-group entity "+entity+": "+this);
+            }
+            Preconditions.checkNotNull(getKeyForMapSensor());
+        }
+
+        protected void setEntityLoadingTargetConfig() {
+            if (getConfig(TARGET_SENSOR)!=null)
+                throw new IllegalArgumentException("Must not set 
"+TARGET_SENSOR+" when using "+this);
+        }
+
+        public void setEntity(EntityLocal entity) {
+            super.setEntity(entity);
+            if (suppressDuplicates==null) {
+                // only publish on changes, unless it is configured otherwise
+                suppressDuplicates = Boolean.TRUE;
+            }
+        }
+
+        private final List<Sensor<?>> SOURCE_SENSORS = 
ImmutableList.<Sensor<?>>of(SERVICE_UP, SERVICE_STATE_ACTUAL);
+        @Override
+        protected Collection<Sensor<?>> getSourceSensors() {
+            return SOURCE_SENSORS;
+        }
+
+        @Override
+        protected void onUpdated() {
+            // override superclass to publish potentially several items
+
+            if (getConfig(DERIVE_SERVICE_PROBLEMS))
+                updateMapSensor(SERVICE_PROBLEMS, computeServiceProblems());
+
+            if (getConfig(DERIVE_SERVICE_NOT_UP))
+                updateMapSensor(SERVICE_NOT_UP_INDICATORS, 
computeServiceNotUp());
+        }
+
+        protected Object computeServiceNotUp() {
+            Map<Entity, Boolean> values = getValues(SERVICE_UP);
+            List<Entity> violators = MutableList.of();
+            for (Map.Entry<Entity, Boolean> state: values.entrySet()) {
+                if (!Boolean.TRUE.equals(state.getValue())) {
+                    violators.add(state.getKey());
+                }
+            }
+
+            QuorumCheck qc = getConfig(UP_QUORUM_CHECK);
+            if (qc!=null) {
+                if (qc.isQuorate(values.size()-violators.size(), 
values.size()))
+                    // quorate
+                    return null;
+
+                if (values.isEmpty()) return "No entities present";
+                if (violators.isEmpty()) return "Not enough entities";
+            } else {
+                if (violators.isEmpty())
+                    return null;
+            }
+
+            if (violators.size()==1) return violators.get(0)+" is not up";
+            if (violators.size()==values.size()) return "None of the entities 
are up";
+            return violators.size()+" entities are not up, including 
"+violators.get(0);
+        }
+
+        protected Object computeServiceProblems() {
+            Map<Entity, Lifecycle> values = getValues(SERVICE_STATE_ACTUAL);
+            int numRunning=0, numOnFire=0;
+            for (Lifecycle state: values.values()) {
+                if (state==Lifecycle.RUNNING) numRunning++;
+                else if (state==Lifecycle.ON_FIRE) numOnFire++;
+            }
+
+            QuorumCheck qc = getConfig(RUNNING_QUORUM_CHECK);
+            if (qc!=null) {
+                if (qc.isQuorate(numRunning, numOnFire+numRunning))
+                    // quorate
+                    return null;
+
+                if (numOnFire==0)
+                    return "Not enough entities running to be quorate";
+            } else {
+                if (numOnFire==0)
+                    return null;
+            }
+
+            return numOnFire+" entit"+Strings.ies(numOnFire)+" are on fire";
+        }
+
+        protected void updateMapSensor(AttributeSensor<Map<String, Object>> 
sensor, Object value) {
+            if (value!=null)
+                updateMapSensorEntry(entity, sensor, getKeyForMapSensor(), 
value);
+            else
+                clearMapSensorEntry(entity, sensor, getKeyForMapSensor());
+        }
+
+        /** not used; see specific `computeXxx` methods, invoked by overridden 
onUpdated */
+        @Override
+        protected Object compute() {
+            return null;
+        }
+    }
+    
+    public static class ComputeServiceIndicatorsFromChildrenAndMembersSpec 
extends 
ExtensibleEnricherSpec<ComputeServiceIndicatorsFromChildrenAndMembers,ComputeServiceIndicatorsFromChildrenAndMembersSpec>
 {
+        private static final long serialVersionUID = -607444925297963712L;
+        
+        protected ComputeServiceIndicatorsFromChildrenAndMembersSpec() {
+            this(ComputeServiceIndicatorsFromChildrenAndMembers.class);
+        }
+        
+        protected ComputeServiceIndicatorsFromChildrenAndMembersSpec(Class<? 
extends ComputeServiceIndicatorsFromChildrenAndMembers> clazz) {
+            super(clazz);
+        }
+
+        public void addTo(Entity entity) {
+            entity.addEnricher(this);
+        }
+
+        public ComputeServiceIndicatorsFromChildrenAndMembersSpec 
checkChildrenAndMembers() {
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_MEMBERS, true);
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_CHILDREN, true);
+            return self();
+        }
+        public ComputeServiceIndicatorsFromChildrenAndMembersSpec 
checkMembersOnly() {
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_MEMBERS, true);
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_CHILDREN, false);
+            return self();
+        }
+        public ComputeServiceIndicatorsFromChildrenAndMembersSpec 
checkChildrenOnly() {
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_MEMBERS, false);
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_CHILDREN, true);
+            return self();
+        }
+
+        public ComputeServiceIndicatorsFromChildrenAndMembersSpec 
requireUpChildren(QuorumCheck check) {
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.UP_QUORUM_CHECK, 
check);
+            return self();
+        }
+        public ComputeServiceIndicatorsFromChildrenAndMembersSpec 
requireRunningChildren(QuorumCheck check) {
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.RUNNING_QUORUM_CHECK, 
check);
+            return self();
+        }
+    }
+
+    /** provides the default {@link 
ComputeServiceIndicatorsFromChildrenAndMembers} enricher, 
+     * using the default unique tag ({@link 
ComputeServiceIndicatorsFromChildrenAndMembers#IDENTIFIER_DEFAULT}),
+     * configured here to require none on fire, and either no children or at 
least one up child,
+     * the spec can be further configured as appropriate */
+    public static ComputeServiceIndicatorsFromChildrenAndMembersSpec 
newEnricherFromChildren() {
+        return new ComputeServiceIndicatorsFromChildrenAndMembersSpec()
+            
.uniqueTag(ComputeServiceIndicatorsFromChildrenAndMembers.IDENTIFIER_DEFAULT);
+    }
+
+    /** as {@link #newEnricherFromChildren()} but only publishing service 
not-up indicators, 
+     * using a different unique tag ({@link 
ComputeServiceIndicatorsFromChildrenAndMembers#IDENTIFIER_UP}),
+     * listening to children only, ignoring lifecycle/service-state,
+     * and using the same logic 
+     * (viz looking only at children (not members) and requiring either no 
children or at least one child up) by default */
+    public static ComputeServiceIndicatorsFromChildrenAndMembersSpec 
newEnricherFromChildrenUp() {
+        return newEnricherFromChildren()
+            
.uniqueTag(ComputeServiceIndicatorsFromChildrenAndMembers.IDENTIFIER_UP)
+            .checkChildrenOnly()
+            
.configure(ComputeServiceIndicatorsFromChildrenAndMembers.DERIVE_SERVICE_PROBLEMS,
 false);
+    }
+    
+    /** as {@link #newEnricherFromChildren()} but only publishing service 
problems,
+     * listening to children and members, ignoring service up,
+     * and using the same logic 
+     * (viz looking at children and members and requiring none are on fire) by 
default */
+    public static ComputeServiceIndicatorsFromChildrenAndMembersSpec 
newEnricherFromChildrenState() {
+        return newEnricherFromChildren()
+            
.configure(ComputeServiceIndicatorsFromChildrenAndMembers.DERIVE_SERVICE_NOT_UP,
 false);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/main/java/brooklyn/entity/basic/ServiceStatusLogic.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/ServiceStatusLogic.java 
b/core/src/main/java/brooklyn/entity/basic/ServiceStatusLogic.java
deleted file mode 100644
index 8f4c6b4..0000000
--- a/core/src/main/java/brooklyn/entity/basic/ServiceStatusLogic.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package brooklyn.entity.basic;
-
-import java.util.Map;
-
-import brooklyn.enricher.Enrichers;
-import brooklyn.enricher.basic.UpdatingMap;
-import brooklyn.event.AttributeSensor;
-import brooklyn.event.Sensor;
-import brooklyn.policy.EnricherSpec;
-import brooklyn.util.collections.CollectionFunctionals;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.guava.Functionals;
-
-import com.google.common.base.Function;
-import com.google.common.base.Functions;
-
-/** Logic, sensors and enrichers, and conveniences, for computing service 
status */ 
-public class ServiceStatusLogic {
-
-    public static final AttributeSensor<Boolean> SERVICE_UP = 
Attributes.SERVICE_UP;
-    public static final AttributeSensor<Map<String,Object>> 
SERVICE_NOT_UP_INDICATORS = Attributes.SERVICE_NOT_UP_INDICATORS;
-    
-    private ServiceStatusLogic() {}
-    
-    public static class ServiceNotUpLogic {
-        @SuppressWarnings({ "unchecked", "rawtypes" })
-        public static final EnricherSpec<?> 
newEnricherForServiceUpIfNoNotUpIndicators() {
-            return Enrichers.builder()
-                
.transforming(SERVICE_NOT_UP_INDICATORS).publishing(Attributes.SERVICE_UP)
-                .computing( /* cast hacks to support removing */ (Function)
-                    Functionals.<Map<String,?>>
-                        
ifNotEquals(null).<Object>apply(Functions.forPredicate(CollectionFunctionals.<String>mapSizeEquals(0)))
-                        .defaultValue(Entities.REMOVE) )
-                .uniqueTag("service.isUp if no service.notUp.indicators")
-                .build();
-        }
-        
-        /** puts the given value into the {@link 
Attributes#SERVICE_NOT_UP_INDICATORS} map as if the 
-         * {@link UpdatingMap} enricher for the given sensor reported this 
value (including {@link Entities#REMOVE}) */
-        public static void updateMapFromSensor(EntityLocal entity, Sensor<?> 
sensor, Object value) {
-            updateMapSensor(entity, Attributes.SERVICE_NOT_UP_INDICATORS, 
sensor.getName(), value);
-        }
-    }
-    
-
-    @SuppressWarnings("unchecked")
-    public static <TKey,TVal> void updateMapSensor(EntityLocal entity, 
AttributeSensor<Map<TKey,TVal>> sensor,
-            TKey key, Object v) {
-        Map<TKey, TVal> map = entity.getAttribute(sensor);
-
-        // TODO synchronize
-        
-        boolean created = (map==null);
-        if (created) map = MutableMap.of();
-                
-        boolean changed;
-        if (v == Entities.REMOVE) {
-            changed = map.containsKey(key);
-            if (changed)
-                map.remove(key);
-        } else {
-            TVal oldV = map.get(key);
-            if (oldV==null)
-                changed = (v!=null || !map.containsKey(key));
-            else
-                changed = !oldV.equals(v);
-            if (changed)
-                map.put(key, (TVal)v);
-        }
-        if (changed || created)
-            entity.setAttribute(sensor, map);
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/main/java/brooklyn/entity/group/DynamicCluster.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/group/DynamicCluster.java 
b/core/src/main/java/brooklyn/entity/group/DynamicCluster.java
index c44f302..f51a9ef 100644
--- a/core/src/main/java/brooklyn/entity/group/DynamicCluster.java
+++ b/core/src/main/java/brooklyn/entity/group/DynamicCluster.java
@@ -103,7 +103,7 @@ public interface DynamicCluster extends AbstractGroup, 
Cluster, MemberReplaceabl
     ConfigKey<Boolean> QUARANTINE_FAILED_ENTITIES = 
ConfigKeys.newBooleanConfigKey(
             "dynamiccluster.quarantineFailedEntities", "If true, will 
quarantine entities that fail to start; if false, will get rid of them (i.e. 
delete them)", true);
 
-    AttributeSensor<Lifecycle> SERVICE_STATE = Attributes.SERVICE_STATE;
+    AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = 
Attributes.SERVICE_STATE_ACTUAL;
 
     BasicNotificationSensor<Entity> ENTITY_QUARANTINED = new 
BasicNotificationSensor<Entity>(Entity.class, 
"dynamiccluster.entityQuarantined", "Entity failed to start, and has been 
quarantined");
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/main/java/brooklyn/entity/group/DynamicClusterImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/group/DynamicClusterImpl.java 
b/core/src/main/java/brooklyn/entity/group/DynamicClusterImpl.java
index 27ffaf8..1023db6 100644
--- a/core/src/main/java/brooklyn/entity/group/DynamicClusterImpl.java
+++ b/core/src/main/java/brooklyn/entity/group/DynamicClusterImpl.java
@@ -23,7 +23,6 @@ import static 
com.google.common.base.Preconditions.checkNotNull;
 
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -40,14 +39,13 @@ import brooklyn.entity.basic.AbstractGroupImpl;
 import brooklyn.entity.basic.Entities;
 import brooklyn.entity.basic.EntityFactory;
 import brooklyn.entity.basic.EntityFactoryForLocation;
-import brooklyn.entity.basic.EntityFunctions;
 import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.QuorumCheck.QuorumChecks;
+import brooklyn.entity.basic.ServiceStateLogic;
 import brooklyn.entity.effector.Effectors;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.entity.trait.Startable;
 import brooklyn.entity.trait.StartableMethods;
-import brooklyn.event.SensorEvent;
-import brooklyn.event.SensorEventListener;
 import brooklyn.location.Location;
 import brooklyn.location.basic.Locations;
 import brooklyn.location.cloud.AvailabilityZoneExtension;
@@ -147,6 +145,17 @@ public class DynamicClusterImpl extends AbstractGroupImpl 
implements DynamicClus
     }
 
     @Override
+    protected void initEnrichers() {
+        if (getConfigRaw(UP_QUORUM_CHECK, true).isAbsent() && 
getConfig(INITIAL_SIZE)==0) {
+            // if initial size is 0 then override up check to allow zero if 
empty
+            setConfig(UP_QUORUM_CHECK, QuorumChecks.atLeastOneUnlessEmpty());
+        }
+        super.initEnrichers();
+        // override previous enricher so that only members are checked
+        
ServiceStateLogic.newEnricherFromChildrenUp().checkMembersOnly().requireUpChildren(getConfig(UP_QUORUM_CHECK)).addTo(this);
+    }
+    
+    @Override
     public void setRemovalStrategy(Function<Collection<Entity>, Entity> val) {
         setConfig(REMOVAL_STRATEGY, checkNotNull(val, "removalStrategy"));
     }
@@ -247,13 +256,15 @@ public class DynamicClusterImpl extends AbstractGroupImpl 
implements DynamicClus
             setAttribute(SUB_LOCATIONS, findSubLocations(loc));
         }
 
-        setAttribute(SERVICE_STATE, Lifecycle.STARTING);
-        setAttribute(SERVICE_UP, calculateServiceUp());
+        ServiceStateLogic.setExpectedState(this, Lifecycle.STARTING);
         try {
             if (isQuarantineEnabled()) {
-                QuarantineGroup quarantineGroup = 
addChild(EntitySpec.create(QuarantineGroup.class).displayName("quarantine"));
-                Entities.manage(quarantineGroup);
-                setAttribute(QUARANTINE_GROUP, quarantineGroup);
+                QuarantineGroup quarantineGroup = 
getAttribute(QUARANTINE_GROUP);
+                if (quarantineGroup==null || 
!Entities.isManaged(quarantineGroup)) {
+                    quarantineGroup = 
addChild(EntitySpec.create(QuarantineGroup.class).displayName("quarantine"));
+                    Entities.manage(quarantineGroup);
+                    setAttribute(QUARANTINE_GROUP, quarantineGroup);
+                }
             }
 
             int initialSize = getConfig(INITIAL_SIZE).intValue();
@@ -307,44 +318,11 @@ public class DynamicClusterImpl extends AbstractGroupImpl 
implements DynamicClus
             for (Policy it : getPolicies()) {
                 it.resume();
             }
-            setAttribute(SERVICE_STATE, Lifecycle.RUNNING);
-            setAttribute(SERVICE_UP, calculateServiceUp());
+            ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
         } catch (Exception e) {
-            setAttribute(SERVICE_STATE, Lifecycle.ON_FIRE);
+            ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
             throw Exceptions.propagate(e);
-        } finally {
-            connectSensors();
-        }
-    }
-
-    protected void connectSensors() {
-        subscribeToChildren(this, SERVICE_STATE, new 
SensorEventListener<Lifecycle>() {
-            @Override
-            public void onEvent(SensorEvent<Lifecycle> event) {
-                setAttribute(SERVICE_STATE, calculateServiceState());
-            }
-        });
-        subscribeToChildren(this, SERVICE_UP, new 
SensorEventListener<Boolean>() {
-            @Override
-            public void onEvent(SensorEvent<Boolean> event) {
-                setAttribute(SERVICE_UP, calculateServiceUp());
-            }
-        });
-    }
-
-    protected Lifecycle calculateServiceState() {
-        Lifecycle currentState = getAttribute(SERVICE_STATE);
-        if (EnumSet.of(Lifecycle.ON_FIRE, 
Lifecycle.RUNNING).contains(currentState)) {
-            Iterable<Lifecycle> memberStates = 
Iterables.transform(getMembers(), EntityFunctions.attribute(SERVICE_STATE));
-            int running = Iterables.frequency(memberStates, Lifecycle.RUNNING);
-            int onFire = Iterables.frequency(memberStates, Lifecycle.ON_FIRE);
-            if ((getInitialQuorumSize() > 0 ? running < getInitialQuorumSize() 
: true) && onFire > 0) {
-                currentState = Lifecycle.ON_FIRE;
-            } else if (onFire == 0 && running > 0) {
-                currentState = Lifecycle.RUNNING;
-            }
         }
-        return currentState;
     }
 
     protected List<Location> findSubLocations(Location loc) {
@@ -386,10 +364,8 @@ public class DynamicClusterImpl extends AbstractGroupImpl 
implements DynamicClus
 
     @Override
     public void stop() {
-        setAttribute(SERVICE_STATE, Lifecycle.STOPPING);
+        ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPING);
         try {
-            setAttribute(SERVICE_UP, calculateServiceUp());
-
             for (Policy it : getPolicies()) { it.suspend(); }
 
             // run shrink without mutex to make things stop even if starting,
@@ -403,10 +379,9 @@ public class DynamicClusterImpl extends AbstractGroupImpl 
implements DynamicClus
             // (this ignores the quarantine node which is not stoppable)
             StartableMethods.stop(this);
 
-            setAttribute(SERVICE_STATE, Lifecycle.STOPPED);
-            setAttribute(SERVICE_UP, calculateServiceUp());
+            ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPED);
         } catch (Exception e) {
-            setAttribute(SERVICE_STATE, Lifecycle.ON_FIRE);
+            ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
             throw Exceptions.propagate(e);
         }
     }
@@ -697,13 +672,6 @@ public class DynamicClusterImpl extends AbstractGroupImpl 
implements DynamicClus
         }
     }
 
-    /**
-     * Default impl is to be up when running, and !up otherwise.
-     */
-    protected boolean calculateServiceUp() {
-        return getAttribute(SERVICE_STATE) == Lifecycle.RUNNING;
-    }
-
     protected Map<Entity, Throwable> waitForTasksOnEntityStart(Map<? extends 
Entity,? extends Task<?>> tasks) {
         // TODO Could have CompoundException, rather than propagating first
         Map<Entity, Throwable> errors = Maps.newLinkedHashMap();

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/main/java/brooklyn/entity/group/DynamicFabric.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/group/DynamicFabric.java 
b/core/src/main/java/brooklyn/entity/group/DynamicFabric.java
index abd7f5f..51735ec 100644
--- a/core/src/main/java/brooklyn/entity/group/DynamicFabric.java
+++ b/core/src/main/java/brooklyn/entity/group/DynamicFabric.java
@@ -65,7 +65,7 @@ public interface DynamicFabric extends AbstractGroup, 
Startable, Fabric {
     public static final MapConfigKey<Object> CUSTOM_CHILD_FLAGS = new 
MapConfigKey<Object>(
             Object.class, "dynamicfabric.customChildFlags", "Additional flags 
to be passed to children when they are being created", 
ImmutableMap.<String,Object>of());
 
-    public static final AttributeSensor<Lifecycle> SERVICE_STATE = 
Attributes.SERVICE_STATE;
+    public static final AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = 
Attributes.SERVICE_STATE_ACTUAL;
 
     public void setMemberSpec(EntitySpec<?> memberSpec);
     

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/main/java/brooklyn/entity/group/DynamicFabricImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/group/DynamicFabricImpl.java 
b/core/src/main/java/brooklyn/entity/group/DynamicFabricImpl.java
index a72f77f..8ba08d0 100644
--- a/core/src/main/java/brooklyn/entity/group/DynamicFabricImpl.java
+++ b/core/src/main/java/brooklyn/entity/group/DynamicFabricImpl.java
@@ -39,6 +39,7 @@ import brooklyn.entity.basic.EntityFactoryForLocation;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.EntityLocal;
 import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
 import brooklyn.entity.effector.Effectors;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.entity.trait.Changeable;
@@ -118,7 +119,7 @@ public class DynamicFabricImpl extends AbstractGroupImpl 
implements DynamicFabri
         if (newLocations.isEmpty()) newLocations.addAll(getLocations());
         int locIndex = 0;
         
-        setAttribute(SERVICE_STATE, Lifecycle.STARTING);
+        ServiceStateLogic.setExpectedState(this, Lifecycle.STARTING);
         try {
             Map<Entity, Task<?>> tasks = Maps.newLinkedHashMap();
             
@@ -158,10 +159,10 @@ public class DynamicFabricImpl extends AbstractGroupImpl 
implements DynamicFabri
             }
             
             waitForTasksOnStart(tasks);
-            setAttribute(SERVICE_STATE, Lifecycle.RUNNING);
+            ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
             setAttribute(SERVICE_UP, true);
         } catch (Exception e) {
-            setAttribute(SERVICE_STATE, Lifecycle.ON_FIRE);
+            ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
             throw Exceptions.propagate(e);
         }
     }
@@ -183,15 +184,15 @@ public class DynamicFabricImpl extends AbstractGroupImpl 
implements DynamicFabri
     
     @Override
     public void stop() {
-        setAttribute(SERVICE_STATE, Lifecycle.STOPPING);
+        ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPING);
         try {
             Iterable<Entity> stoppableChildren = 
Iterables.filter(getChildren(), Predicates.instanceOf(Startable.class));
             Task<?> invoke = Entities.invokeEffector(this, stoppableChildren, 
Startable.STOP);
                if (invoke != null) invoke.get();
-            setAttribute(SERVICE_STATE, Lifecycle.STOPPED);
+               ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPED);
             setAttribute(SERVICE_UP, false);
         } catch (Exception e) {
-            setAttribute(SERVICE_STATE, Lifecycle.ON_FIRE);
+            ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
             throw Exceptions.propagate(e);
         }
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/main/java/brooklyn/event/basic/BasicConfigKey.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/basic/BasicConfigKey.java 
b/core/src/main/java/brooklyn/event/basic/BasicConfigKey.java
index 5bb864d..91420e5 100644
--- a/core/src/main/java/brooklyn/event/basic/BasicConfigKey.java
+++ b/core/src/main/java/brooklyn/event/basic/BasicConfigKey.java
@@ -227,8 +227,12 @@ public class BasicConfigKey<T> implements 
ConfigKeySelfExtracting<T>, Serializab
         private final ConfigKey<T> parentKey;
         
         public BasicConfigKeyOverwriting(ConfigKey<T> key, T defaultValue) {
+            this(key, key.getDescription(), defaultValue);
+        }
+        
+        public BasicConfigKeyOverwriting(ConfigKey<T> key, String 
newDescription, T defaultValue) {
             super(checkNotNull(key.getTypeToken(), "type"), 
checkNotNull(key.getName(), "name"), 
-                    key.getDescription(), defaultValue);
+                    newDescription, defaultValue);
             parentKey = key;
         }
         

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/main/java/brooklyn/event/basic/DependentConfiguration.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/brooklyn/event/basic/DependentConfiguration.java 
b/core/src/main/java/brooklyn/event/basic/DependentConfiguration.java
index 8630214..9d8bf4b 100644
--- a/core/src/main/java/brooklyn/event/basic/DependentConfiguration.java
+++ b/core/src/main/java/brooklyn/event/basic/DependentConfiguration.java
@@ -401,12 +401,12 @@ public class DependentConfiguration {
         
         /**
          * Will wait for the attribute on the given entity.
-         * If that entity report {@link Lifecycle#ON_FIRE} for its {@link 
Attributes#SERVICE_STATE} then it will abort. 
+         * If that entity report {@link Lifecycle#ON_FIRE} for its {@link 
Attributes#SERVICE_STATE_ACTUAL} then it will abort. 
          */
         public <T2> Builder<T2,T2> attributeWhenReady(Entity source, 
AttributeSensor<T2> sensor) {
             this.source = checkNotNull(source, "source");
             this.sensor = (AttributeSensor) checkNotNull(sensor, "sensor");
-            abortIf(source, Attributes.SERVICE_STATE, 
Predicates.equalTo(Lifecycle.ON_FIRE));
+            abortIf(source, Attributes.SERVICE_STATE_ACTUAL, 
Predicates.equalTo(Lifecycle.ON_FIRE));
             return (Builder<T2, T2>) this;
         }
         /** returns a task for parallel execution returning a list of values 
of the given sensor list on the given entity, 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/main/java/brooklyn/policy/basic/AbstractEntityAdjunct.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/brooklyn/policy/basic/AbstractEntityAdjunct.java 
b/core/src/main/java/brooklyn/policy/basic/AbstractEntityAdjunct.java
index 68f8162..6b4d49d 100644
--- a/core/src/main/java/brooklyn/policy/basic/AbstractEntityAdjunct.java
+++ b/core/src/main/java/brooklyn/policy/basic/AbstractEntityAdjunct.java
@@ -377,6 +377,7 @@ public abstract class AbstractEntityAdjunct extends 
AbstractBrooklynObject imple
                 .add("name", name)
                 .add("uniqueTag", uniqueTag)
                 .add("running", isRunning())
+                .add("entity", entity)
                 .add("id", getId())
                 .toString();
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/enricher/basic/BasicEnricherTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/enricher/basic/BasicEnricherTest.java 
b/core/src/test/java/brooklyn/enricher/basic/BasicEnricherTest.java
index 1252e36..59bc7e8 100644
--- a/core/src/test/java/brooklyn/enricher/basic/BasicEnricherTest.java
+++ b/core/src/test/java/brooklyn/enricher/basic/BasicEnricherTest.java
@@ -22,11 +22,15 @@ import static org.testng.Assert.assertEquals;
 
 import java.util.Map;
 
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.Iterables;
+
 import brooklyn.config.ConfigKey;
 import brooklyn.entity.BrooklynAppUnitTestSupport;
 import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.policy.Enricher;
 import brooklyn.policy.EnricherSpec;
 import brooklyn.util.collections.MutableSet;
 import brooklyn.util.flags.SetFromFlag;
@@ -91,10 +95,14 @@ public class BasicEnricherTest extends 
BrooklynAppUnitTestSupport {
 
     @Test
     public void testSameUniqueTagEnricherNotAddedTwice() throws Exception {
-        MyEnricher enricher1 = 
app.addEnricher(EnricherSpec.create(MyEnricher.class).tag(99).uniqueTag("x"));
-        MyEnricher enricher2 = 
app.addEnricher(EnricherSpec.create(MyEnricher.class).tag(94).uniqueTag("x"));
-        assertEquals(enricher2, enricher1);
+        
app.addEnricher(EnricherSpec.create(MyEnricher.class).tag(99).uniqueTag("x"));
+        
app.addEnricher(EnricherSpec.create(MyEnricher.class).tag(94).uniqueTag("x"));
+        
         assertEquals(app.getEnrichers().size(), 1);
+        // the more recent one should dominate
+        Enricher enricher = Iterables.getOnlyElement(app.getEnrichers());
+        Assert.assertTrue(enricher.getTagSupport().containsTag(94));
+        Assert.assertFalse(enricher.getTagSupport().containsTag(99));
     }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/entity/basic/DependentConfigurationTest.java
----------------------------------------------------------------------
diff --git 
a/core/src/test/java/brooklyn/entity/basic/DependentConfigurationTest.java 
b/core/src/test/java/brooklyn/entity/basic/DependentConfigurationTest.java
index 3c4acbb..f6096a4 100644
--- a/core/src/test/java/brooklyn/entity/basic/DependentConfigurationTest.java
+++ b/core/src/test/java/brooklyn/entity/basic/DependentConfigurationTest.java
@@ -192,7 +192,7 @@ public class DependentConfigurationTest extends 
BrooklynAppUnitTestSupport {
                 .attributeWhenReady(entity, TestEntity.NAME)
                 .build());
 
-        entity.setAttribute(Attributes.SERVICE_STATE, Lifecycle.ON_FIRE);
+        ServiceStateLogic.setExpectedState(entity, Lifecycle.ON_FIRE);
         try {
             assertDoneEventually(t);
             fail();
@@ -203,7 +203,7 @@ public class DependentConfigurationTest extends 
BrooklynAppUnitTestSupport {
 
     @Test
     public void testAttributeWhenReadyAbortsWhenAlreadyOnfireByDefault() 
throws Exception {
-        entity.setAttribute(Attributes.SERVICE_STATE, Lifecycle.ON_FIRE);
+        ServiceStateLogic.setExpectedState(entity, Lifecycle.ON_FIRE);
         
         final Task<String> t = submit(DependentConfiguration.builder()
                 .attributeWhenReady(entity, TestEntity.NAME)

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/entity/basic/DynamicGroupTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/basic/DynamicGroupTest.java 
b/core/src/test/java/brooklyn/entity/basic/DynamicGroupTest.java
index 12c3b55..b52332b 100644
--- a/core/src/test/java/brooklyn/entity/basic/DynamicGroupTest.java
+++ b/core/src/test/java/brooklyn/entity/basic/DynamicGroupTest.java
@@ -369,7 +369,6 @@ public class DynamicGroupTest {
         };
         ((EntityLocal)group2).setConfig(DynamicGroup.ENTITY_FILTER, 
Predicates.instanceOf(TestEntity.class));
         app.addChild(group2);
-        group2.init();
         Entities.manage(group2);
         
         for (int i = 0; i < NUM_CYCLES; i++) {
@@ -421,7 +420,6 @@ public class DynamicGroupTest {
         };
         ((EntityLocal)group2).setConfig(DynamicGroup.ENTITY_FILTER, 
Predicates.<Object>equalTo(e3));
         app.addChild(group2);
-        group2.init();
         
         Thread t1 = new Thread(new Runnable() {
             @Override public void run() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/entity/basic/EntitySpecTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/basic/EntitySpecTest.java 
b/core/src/test/java/brooklyn/entity/basic/EntitySpecTest.java
index 6363a81..0a86e31 100644
--- a/core/src/test/java/brooklyn/entity/basic/EntitySpecTest.java
+++ b/core/src/test/java/brooklyn/entity/basic/EntitySpecTest.java
@@ -37,6 +37,7 @@ import brooklyn.policy.PolicySpec;
 import brooklyn.policy.basic.AbstractPolicy;
 import brooklyn.test.Asserts;
 import brooklyn.test.entity.TestEntity;
+import brooklyn.test.entity.TestEntityNoEnrichersImpl;
 import brooklyn.util.flags.SetFromFlag;
 
 import com.google.common.collect.ImmutableSet;
@@ -104,7 +105,7 @@ public class EntitySpecTest extends 
BrooklynAppUnitTestSupport {
     
     @Test
     public void testAddsEnricherSpec() throws Exception {
-        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class)
+        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class, 
TestEntityNoEnrichersImpl.class)
                 .enricher(EnricherSpec.create(MyEnricher.class)
                         .displayName("myenrichername")
                         .configure(MyEnricher.CONF1, "myconf1val")
@@ -119,7 +120,7 @@ public class EntitySpecTest extends 
BrooklynAppUnitTestSupport {
     @Test
     public void testAddsEnricher() throws Exception {
         MyEnricher enricher = new MyEnricher();
-        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class)
+        entity = app.createAndManageChild(EntitySpec.create(TestEntity.class, 
TestEntityNoEnrichersImpl.class)
                 .enricher(enricher));
         
         assertEquals(Iterables.getOnlyElement(entity.getEnrichers()), 
enricher);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/entity/basic/PolicyRegistrationTest.java
----------------------------------------------------------------------
diff --git 
a/core/src/test/java/brooklyn/entity/basic/PolicyRegistrationTest.java 
b/core/src/test/java/brooklyn/entity/basic/PolicyRegistrationTest.java
index ae6b384..168f72c 100644
--- a/core/src/test/java/brooklyn/entity/basic/PolicyRegistrationTest.java
+++ b/core/src/test/java/brooklyn/entity/basic/PolicyRegistrationTest.java
@@ -38,6 +38,7 @@ import brooklyn.policy.PolicySpec;
 import brooklyn.policy.basic.AbstractPolicy;
 import brooklyn.test.TestUtils;
 import brooklyn.test.entity.TestEntity;
+import brooklyn.test.entity.TestEntityNoEnrichersImpl;
 import brooklyn.util.collections.MutableMap;
 
 import com.google.common.collect.ImmutableList;
@@ -116,9 +117,10 @@ public class PolicyRegistrationTest extends 
BrooklynAppUnitTestSupport {
     
     @Test
     public void testAddEnricherSpec() {
-        EntitySpecTest.MyEnricher enricher = 
entity.addEnricher(EnricherSpec.create(EntitySpecTest.MyEnricher.class));
+        TestEntity entity2 = 
app.createAndManageChild(EntitySpec.create(TestEntity.class, 
TestEntityNoEnrichersImpl.class));
+        EntitySpecTest.MyEnricher enricher = 
entity2.addEnricher(EnricherSpec.create(EntitySpecTest.MyEnricher.class));
         assertNotNull(enricher);
-        assertEquals(entity.getEnrichers(), ImmutableList.of(enricher));
+        assertEquals(entity2.getEnrichers(), ImmutableList.of(enricher));
     }
 
     @Test

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.java 
b/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.java
index c862f8a..ed1d7b6 100644
--- a/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.java
+++ b/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.java
@@ -57,10 +57,12 @@ import brooklyn.location.Location;
 import brooklyn.location.basic.SimulatedLocation;
 import brooklyn.management.Task;
 import brooklyn.test.Asserts;
+import brooklyn.test.EntityTestUtils;
 import brooklyn.test.entity.TestEntity;
 import brooklyn.test.entity.TestEntityImpl;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.time.Duration;
 import brooklyn.util.time.Time;
 
 import com.google.common.base.Function;
@@ -143,7 +145,8 @@ public class DynamicClusterTest extends 
BrooklynAppUnitTestSupport {
                 .configure(DynamicCluster.MEMBER_SPEC, 
EntitySpec.create(TestEntity.class))
                 .configure(DynamicCluster.INITIAL_SIZE, 0));
         cluster.start(ImmutableList.of(loc));
-        assertEquals(cluster.getAttribute(Attributes.SERVICE_STATE), 
Lifecycle.RUNNING);
+        
+        EntityTestUtils.assertAttributeEqualsEventually(cluster, 
Attributes.SERVICE_STATE_ACTUAL, Lifecycle.RUNNING);
         assertTrue(cluster.getAttribute(Attributes.SERVICE_UP));
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/entity/rebind/RebindEnricherTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindEnricherTest.java 
b/core/src/test/java/brooklyn/entity/rebind/RebindEnricherTest.java
index 27e5edc..fc89ec3 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindEnricherTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindEnricherTest.java
@@ -286,8 +286,8 @@ public class RebindEnricherTest extends 
RebindTestFixtureWithApp {
     
     public static class MyTestEntityWithEnricher extends TestEntityImpl {
         @Override
-        public void init() {
-            super.init();
+        protected void initEnrichers() {
+            // don't add default ones
             
addEnricher(EnricherSpec.create(MyEnricher.class).uniqueTag("x").tag(Identifiers.makeRandomId(8)));
             addEnricher(EnricherSpec.create(MyEnricher.class));
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterTestFixture.java
----------------------------------------------------------------------
diff --git 
a/core/src/test/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterTestFixture.java
 
b/core/src/test/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterTestFixture.java
index 4b22592..c71eaa8 100644
--- 
a/core/src/test/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterTestFixture.java
+++ 
b/core/src/test/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterTestFixture.java
@@ -122,7 +122,7 @@ public abstract class BrooklynMementoPersisterTestFixture {
         assertTrue(Iterables.contains(reloadedMemento.getEntityIds(), 
entity.getId()));
         
assertEquals(Iterables.getOnlyElement(reloadedMemento.getLocationIds()), 
location.getId());
         assertEquals(Iterables.getOnlyElement(reloadedMemento.getPolicyIds()), 
policy.getId());
-        
assertEquals(Iterables.getOnlyElement(reloadedMemento.getEnricherIds()), 
enricher.getId());
+        
assertTrue(reloadedMemento.getEnricherIds().contains(enricher.getId()));
     }
 
     @Test

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/event/feed/http/HttpFeedTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/event/feed/http/HttpFeedTest.java 
b/core/src/test/java/brooklyn/event/feed/http/HttpFeedTest.java
index 1a9cf9f..f112890 100644
--- a/core/src/test/java/brooklyn/event/feed/http/HttpFeedTest.java
+++ b/core/src/test/java/brooklyn/event/feed/http/HttpFeedTest.java
@@ -47,6 +47,7 @@ import brooklyn.test.Asserts;
 import brooklyn.test.entity.TestEntity;
 import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
+import brooklyn.util.guava.Functionals;
 import brooklyn.util.http.BetterMockWebServer;
 import brooklyn.util.http.HttpToolResponse;
 import brooklyn.util.time.Duration;
@@ -381,9 +382,9 @@ public class HttpFeedTest extends 
BrooklynAppUnitTestSupport {
                             return null;
                         }
                     })
-                    
.onFailureOrException(EntityFunctions.settingSensorsConstantFunction(entity, 
MutableMap.<AttributeSensor<?>,Object>of(
+                    
.onFailureOrException(Functionals.function(EntityFunctions.settingSensorsConstant(entity,
 MutableMap.<AttributeSensor<?>,Object>of(
                         SENSOR_INT, -1, 
-                        SENSOR_STRING, PollConfig.REMOVE)))
+                        SENSOR_STRING, PollConfig.REMOVE))))
                 .period(100))
                 .build();
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/qa/longevity/EntityCleanupLongevityTestFixture.java
----------------------------------------------------------------------
diff --git 
a/core/src/test/java/brooklyn/qa/longevity/EntityCleanupLongevityTestFixture.java
 
b/core/src/test/java/brooklyn/qa/longevity/EntityCleanupLongevityTestFixture.java
index fab5aa3..c3e6dce 100644
--- 
a/core/src/test/java/brooklyn/qa/longevity/EntityCleanupLongevityTestFixture.java
+++ 
b/core/src/test/java/brooklyn/qa/longevity/EntityCleanupLongevityTestFixture.java
@@ -131,7 +131,7 @@ public abstract class EntityCleanupLongevityTestFixture {
         // TODO would like to assert this
 //        Assert.assertTrue( schedulers.isEmpty(), "Not empty schedulers: 
"+schedulers);
         // but weaker form for now
-        Assert.assertTrue( schedulers.size() <= iterations, "Not empty 
schedulers: "+schedulers);
+        Assert.assertTrue( schedulers.size() <= 3*iterations, "Not empty 
schedulers: "+schedulers.size()+" after "+iterations+", "+schedulers);
         
         // memory leak detection only applies to subclasses who run lots of 
iterations
         if (checkMemoryLeaks())

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/test/entity/TestEntityImpl.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/test/entity/TestEntityImpl.java 
b/core/src/test/java/brooklyn/test/entity/TestEntityImpl.java
index 64131f5..c2a3884 100644
--- a/core/src/test/java/brooklyn/test/entity/TestEntityImpl.java
+++ b/core/src/test/java/brooklyn/test/entity/TestEntityImpl.java
@@ -69,7 +69,7 @@ public class TestEntityImpl extends AbstractEntity implements 
TestEntity {
         return super.configure(flags);
     }
     
-    @Override
+    @Override // made public for testing
     public boolean isLegacyConstruction() {
         return super.isLegacyConstruction();
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/core/src/test/java/brooklyn/test/entity/TestEntityNoEnrichersImpl.java
----------------------------------------------------------------------
diff --git 
a/core/src/test/java/brooklyn/test/entity/TestEntityNoEnrichersImpl.java 
b/core/src/test/java/brooklyn/test/entity/TestEntityNoEnrichersImpl.java
new file mode 100644
index 0000000..66ef7ae
--- /dev/null
+++ b/core/src/test/java/brooklyn/test/entity/TestEntityNoEnrichersImpl.java
@@ -0,0 +1,32 @@
+/*
+ * 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.test.entity;
+
+
+/**
+ * Mock entity for testing.
+ */
+public class TestEntityNoEnrichersImpl extends TestEntityImpl {
+
+    @Override
+    protected void initEnrichers() {
+        // no enrichers here, so we can test the explicit enrichers we set
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/examples/simple-nosql-cluster/src/main/java/brooklyn/demo/CumulusRDFApplication.java
----------------------------------------------------------------------
diff --git 
a/examples/simple-nosql-cluster/src/main/java/brooklyn/demo/CumulusRDFApplication.java
 
b/examples/simple-nosql-cluster/src/main/java/brooklyn/demo/CumulusRDFApplication.java
index 46b00bb..a54ef58 100644
--- 
a/examples/simple-nosql-cluster/src/main/java/brooklyn/demo/CumulusRDFApplication.java
+++ 
b/examples/simple-nosql-cluster/src/main/java/brooklyn/demo/CumulusRDFApplication.java
@@ -36,8 +36,10 @@ 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.basic.SoftwareProcess;
 import brooklyn.entity.basic.StartableApplication;
+import brooklyn.entity.basic.ServiceStateLogic.ServiceNotUpLogic;
 import brooklyn.entity.effector.EffectorBody;
 import brooklyn.entity.effector.Effectors;
 import brooklyn.entity.java.UsesJava;
@@ -115,6 +117,8 @@ public class CumulusRDFApplication extends 
AbstractApplication {
      */
     @Override
     public void init() {
+        super.init();
+        
         // Cassandra cluster
         EntitySpec<CassandraDatacenter> clusterSpec = 
EntitySpec.create(CassandraDatacenter.class)
                 .configure(CassandraDatacenter.MEMBER_SPEC, 
EntitySpec.create(CassandraNode.class)
@@ -204,17 +208,15 @@ public class CumulusRDFApplication extends 
AbstractApplication {
         // TODO use a multi-region web cluster
         Collection<? extends Location> first = 
MutableList.copyOf(Iterables.limit(locations, 1));
 
-        setAttribute(Attributes.SERVICE_STATE, Lifecycle.STARTING);
+        ServiceStateLogic.setExpectedState(this, Lifecycle.STARTING);
         try {
             Entities.invokeEffector(this, cassandra, Startable.START, 
MutableMap.of("locations", locations)).getUnchecked();
             Entities.invokeEffector(this, webapp, Startable.START, 
MutableMap.of("locations", first)).getUnchecked();
         } catch (Exception e) {
-            setAttribute(Attributes.SERVICE_STATE, Lifecycle.ON_FIRE);
             throw Exceptions.propagate(e);
+        } finally {
+            ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
         }
-        setAttribute(SERVICE_UP, true);
-        setAttribute(Attributes.SERVICE_STATE, Lifecycle.RUNNING);
-
         log.info("Started CumulusRDF in " + locations);
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/examples/simple-web-cluster/src/main/java/brooklyn/demo/WebClusterDatabaseExample.java
----------------------------------------------------------------------
diff --git 
a/examples/simple-web-cluster/src/main/java/brooklyn/demo/WebClusterDatabaseExample.java
 
b/examples/simple-web-cluster/src/main/java/brooklyn/demo/WebClusterDatabaseExample.java
index f1ed949..bfd3a1c 100644
--- 
a/examples/simple-web-cluster/src/main/java/brooklyn/demo/WebClusterDatabaseExample.java
+++ 
b/examples/simple-web-cluster/src/main/java/brooklyn/demo/WebClusterDatabaseExample.java
@@ -70,6 +70,8 @@ public class WebClusterDatabaseExample extends 
AbstractApplication {
 
     @Override
     public void init() {
+        super.init();
+        
         MySqlNode mysql = addChild(EntitySpec.create(MySqlNode.class)
                 .configure("creationScriptUrl", DB_SETUP_SQL_URL));
         

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/examples/simple-web-cluster/src/main/java/brooklyn/demo/WebClusterDatabaseExampleApp.java
----------------------------------------------------------------------
diff --git 
a/examples/simple-web-cluster/src/main/java/brooklyn/demo/WebClusterDatabaseExampleApp.java
 
b/examples/simple-web-cluster/src/main/java/brooklyn/demo/WebClusterDatabaseExampleApp.java
index 672158e..b1d3915 100644
--- 
a/examples/simple-web-cluster/src/main/java/brooklyn/demo/WebClusterDatabaseExampleApp.java
+++ 
b/examples/simple-web-cluster/src/main/java/brooklyn/demo/WebClusterDatabaseExampleApp.java
@@ -118,6 +118,8 @@ public class WebClusterDatabaseExampleApp extends 
AbstractApplication implements
 
     @Override
     public void init() {
+        super.init();
+        
         MySqlNode mysql = addChild(
                 EntitySpec.create(MySqlNode.class)
                         .configure(MySqlNode.CREATION_SCRIPT_URL, 
Entities.getRequiredUrlConfig(this, DB_SETUP_SQL_URL)));

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/software/base/src/main/java/brooklyn/entity/basic/AbstractSoftwareProcessDriver.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/main/java/brooklyn/entity/basic/AbstractSoftwareProcessDriver.java
 
b/software/base/src/main/java/brooklyn/entity/basic/AbstractSoftwareProcessDriver.java
index d275418..e9ab449 100644
--- 
a/software/base/src/main/java/brooklyn/entity/basic/AbstractSoftwareProcessDriver.java
+++ 
b/software/base/src/main/java/brooklyn/entity/basic/AbstractSoftwareProcessDriver.java
@@ -136,7 +136,7 @@ public abstract class AbstractSoftwareProcessDriver 
implements SoftwareProcessDr
             DynamicTasks.markInessential();
             boolean previouslyRunning = isRunning();
             try {
-                getEntity().setAttribute(Attributes.SERVICE_STATE, 
Lifecycle.STOPPING);
+                ServiceStateLogic.setExpectedState(getEntity(), 
Lifecycle.STOPPING);
                 stop();
             } catch (Exception e) {
                 // queue a failed task so that there is visual indication that 
this task had a failure,
@@ -154,11 +154,11 @@ public abstract class AbstractSoftwareProcessDriver 
implements SoftwareProcessDr
 
         if (doFullStartOnRestart()) {
             DynamicTasks.waitForLast();
-            getEntity().setAttribute(Attributes.SERVICE_STATE, 
Lifecycle.STARTING);
+            ServiceStateLogic.setExpectedState(getEntity(), 
Lifecycle.STARTING);
             start();
         } else {
             DynamicTasks.queue("launch", new Runnable() { public void run() {
-                getEntity().setAttribute(Attributes.SERVICE_STATE, 
Lifecycle.STARTING);
+                ServiceStateLogic.setExpectedState(getEntity(), 
Lifecycle.STARTING);
                 launch();
             }});
             DynamicTasks.queue("post-launch", new Runnable() { public void 
run() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/software/base/src/main/java/brooklyn/entity/basic/SameServerEntity.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/main/java/brooklyn/entity/basic/SameServerEntity.java 
b/software/base/src/main/java/brooklyn/entity/basic/SameServerEntity.java
index 05fab5e..f7ddddd 100644
--- a/software/base/src/main/java/brooklyn/entity/basic/SameServerEntity.java
+++ b/software/base/src/main/java/brooklyn/entity/basic/SameServerEntity.java
@@ -51,7 +51,7 @@ public interface SameServerEntity extends Entity, Startable {
             "provisioning.properties", "Custom properties to be passed in when 
provisioning a new machine",
             MutableMap.<String, Object>of());
     
-    AttributeSensor<Lifecycle> SERVICE_STATE = Attributes.SERVICE_STATE;
+    AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = 
Attributes.SERVICE_STATE_ACTUAL;
 
     @SuppressWarnings("rawtypes")
     AttributeSensor<MachineProvisioningLocation> PROVISIONING_LOCATION = new 
BasicAttributeSensor<MachineProvisioningLocation>(

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcess.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcess.java 
b/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcess.java
index eb90660..0551840 100644
--- a/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcess.java
+++ b/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcess.java
@@ -156,7 +156,7 @@ public interface SoftwareProcess extends Entity, Startable {
     public static final AttributeSensor<Boolean> SERVICE_PROCESS_IS_RUNNING = 
Sensors.newBooleanSensor("service.process.isRunning", 
         "Whether the process for the service is confirmed as running");
     
-    public static final AttributeSensor<Lifecycle> SERVICE_STATE = 
Attributes.SERVICE_STATE;
+    public static final AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = 
Attributes.SERVICE_STATE_ACTUAL;
  
     public static final AttributeSensor<String> PID_FILE = 
Sensors.newStringSensor("softwareprocess.pid.file", "PID file");
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcessDriverLifecycleEffectorTasks.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcessDriverLifecycleEffectorTasks.java
 
b/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcessDriverLifecycleEffectorTasks.java
index 797d4a3..60e3eb0 100644
--- 
a/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcessDriverLifecycleEffectorTasks.java
+++ 
b/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcessDriverLifecycleEffectorTasks.java
@@ -63,8 +63,7 @@ public class SoftwareProcessDriverLifecycleEffectorTasks 
extends MachineLifecycl
         entity().getDriver().restart();
         DynamicTasks.queue("post-restart", new Runnable() { public void run() {
             postStartCustom();
-            if (entity().getAttribute(Attributes.SERVICE_STATE) == 
Lifecycle.STARTING) 
-                entity().setAttribute(Attributes.SERVICE_STATE, 
Lifecycle.RUNNING);
+            ServiceStateLogic.setExpectedState(entity(), Lifecycle.RUNNING);
         }});
     }
     

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcessImpl.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcessImpl.java 
b/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcessImpl.java
index 25c23df..e264dab 100644
--- a/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcessImpl.java
+++ b/software/base/src/main/java/brooklyn/entity/basic/SoftwareProcessImpl.java
@@ -34,7 +34,6 @@ import org.slf4j.LoggerFactory;
 import brooklyn.config.ConfigKey;
 import brooklyn.enricher.Enrichers;
 import brooklyn.entity.Entity;
-import brooklyn.entity.basic.ServiceStatusLogic.ServiceNotUpLogic;
 import brooklyn.entity.drivers.DriverDependentEntity;
 import brooklyn.entity.drivers.EntityDriverManager;
 import brooklyn.event.feed.function.FunctionFeed;
@@ -116,17 +115,14 @@ public abstract class SoftwareProcessImpl extends 
AbstractEntity implements Soft
     protected MachineLocation getMachineOrNull() {
         return Iterables.get(Iterables.filter(getLocations(), 
MachineLocation.class), 0, null);
     }
-    
+
     @Override
-    public void init() {
-        super.init();
-        
+    protected void initEnrichers() {
+        super.initEnrichers();
         
addEnricher(Enrichers.builder().updatingMap(Attributes.SERVICE_NOT_UP_INDICATORS)
             .from(SERVICE_PROCESS_IS_RUNNING)
             .computing(Functionals.ifNotEquals(true).value("The software 
process for this entity does not appear to be running"))
             .build());
-        
-        
addEnricher(ServiceNotUpLogic.newEnricherForServiceUpIfNoNotUpIndicators());
     }
     
        /**
@@ -251,11 +247,13 @@ public abstract class SoftwareProcessImpl extends 
AbstractEntity implements Soft
     public void onManagementStarting() {
         super.onManagementStarting();
         
-        Lifecycle state = getAttribute(SERVICE_STATE);
+        Lifecycle state = getAttribute(SERVICE_STATE_ACTUAL);
         if (state == null || state == Lifecycle.CREATED) {
             // Expect this is a normal start() sequence (i.e. start() will 
subsequently be called)
             setAttribute(SERVICE_UP, false);
-            setAttribute(SERVICE_STATE, Lifecycle.CREATED);
+            ServiceStateLogic.setExpectedState(this, Lifecycle.CREATED);
+            // force actual to be created because this is expected subsequently
+            setAttribute(SERVICE_STATE_ACTUAL, Lifecycle.CREATED);
        }
     }
        
@@ -263,7 +261,7 @@ public abstract class SoftwareProcessImpl extends 
AbstractEntity implements Soft
     public void onManagementStarted() {
         super.onManagementStarted();
         
-        Lifecycle state = getAttribute(SERVICE_STATE);
+        Lifecycle state = getAttribute(SERVICE_STATE_ACTUAL);
         if (state != null && state != Lifecycle.CREATED) {
             postRebind();
         }
@@ -271,7 +269,7 @@ public abstract class SoftwareProcessImpl extends 
AbstractEntity implements Soft
     
     @Override
     public void rebind() {
-        Lifecycle state = getAttribute(SERVICE_STATE);
+        Lifecycle state = getAttribute(SERVICE_STATE_ACTUAL);
         if (state == null || state != Lifecycle.RUNNING) {
             log.warn("On rebind of {}, not rebinding because state is {}", 
this, state);
             return;
@@ -309,11 +307,12 @@ public abstract class SoftwareProcessImpl extends 
AbstractEntity implements Soft
         Entities.waitForServiceUp(this, Duration.of(duration, units));
     }
 
+    /** @deprecated since 0.7.0, this isn't a general test for modifiability, 
and was hardly ever used (now never used) */
+    @Deprecated
     public void checkModifiable() {
-        Lifecycle state = getAttribute(SERVICE_STATE);
-        if (getAttribute(SERVICE_STATE) == Lifecycle.RUNNING) return;
-        if (getAttribute(SERVICE_STATE) == Lifecycle.STARTING) return;
-        // TODO this check may be redundant or even inappropriate
+        Lifecycle state = getAttribute(SERVICE_STATE_ACTUAL);
+        if (getAttribute(SERVICE_STATE_ACTUAL) == Lifecycle.RUNNING) return;
+        if (getAttribute(SERVICE_STATE_ACTUAL) == Lifecycle.STARTING) return;
         throw new IllegalStateException("Cannot configure entity "+this+" in 
state "+state);
     }
 
@@ -400,20 +399,21 @@ public abstract class SoftwareProcessImpl extends 
AbstractEntity implements Soft
             try {
                 isRunningResult = driver.isRunning();
             } catch (Exception  e) {
-                setAttribute(SERVICE_STATE, Lifecycle.ON_FIRE);
+                ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
                 // provide extra context info, as we're seeing this happen in 
strange circumstances
                 if (driver==null) throw new IllegalStateException(this+" 
concurrent start and shutdown detected");
                 throw new IllegalStateException("Error detecting whether 
"+this+" is running: "+e, e);
             }
             if (log.isDebugEnabled()) log.debug("checked {}, is running 
returned: {}", this, isRunningResult);
-            // slow exponential delay -- 1.1^N means after 40 tries and 50s 
elapsed, it reaches the max of 5s intervals  
+            // slow exponential delay -- 1.1^N means after 40 tries and 50s 
elapsed, it reaches the max of 5s intervals
+            // TODO use Repeater 
             delay = Math.min(delay*11/10, 5000);
         }
         if (!isRunningResult) {
             String msg = "Software process entity "+this+" did not pass 
is-running check within "+
                     "the required "+startTimeout+" limit 
("+timer.getDurationElapsed().toStringRounded()+" elapsed)";
             log.warn(msg+" (throwing)");
-            setAttribute(SERVICE_STATE, Lifecycle.ON_FIRE);
+            ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
             throw new IllegalStateException(msg);
         }
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/software/base/src/main/java/brooklyn/entity/brooklynnode/BrooklynEntityMirrorImpl.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/main/java/brooklyn/entity/brooklynnode/BrooklynEntityMirrorImpl.java
 
b/software/base/src/main/java/brooklyn/entity/brooklynnode/BrooklynEntityMirrorImpl.java
index 8407708..24c3eeb 100644
--- 
a/software/base/src/main/java/brooklyn/entity/brooklynnode/BrooklynEntityMirrorImpl.java
+++ 
b/software/base/src/main/java/brooklyn/entity/brooklynnode/BrooklynEntityMirrorImpl.java
@@ -33,9 +33,7 @@ import brooklyn.entity.basic.Attributes;
 import brooklyn.entity.basic.BrooklynTaskTags;
 import brooklyn.entity.basic.Entities;
 import brooklyn.entity.basic.EntityFunctions;
-import brooklyn.entity.basic.Lifecycle;
 import brooklyn.entity.effector.EffectorBody;
-import brooklyn.event.AttributeSensor;
 import brooklyn.event.basic.Sensors;
 import brooklyn.event.feed.http.HttpFeed;
 import brooklyn.event.feed.http.HttpPollConfig;
@@ -43,6 +41,7 @@ import brooklyn.util.collections.Jsonya;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.config.ConfigBag;
 import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.guava.Functionals;
 import brooklyn.util.http.HttpTool;
 import brooklyn.util.http.HttpTool.HttpClientBuilder;
 import brooklyn.util.http.HttpToolResponse;
@@ -53,6 +52,7 @@ import brooklyn.util.task.Tasks;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Suppliers;
 import com.google.gson.Gson;
 
 public class BrooklynEntityMirrorImpl extends AbstractEntity implements 
BrooklynEntityMirror {
@@ -90,10 +90,9 @@ public class BrooklynEntityMirrorImpl extends AbstractEntity 
implements Brooklyn
             .period(getConfig(POLL_PERIOD))
             .poll(HttpPollConfig.forMultiple()
                 .onSuccess(mirrorSensors)
-                
.onFailureOrException(EntityFunctions.settingSensorsConstantFunction(this, 
MutableMap.<AttributeSensor<?>,Object>of(
-                    Attributes.SERVICE_STATE, Lifecycle.ON_FIRE,
-                    MIRROR_STATUS, "error contacting service"
-                    ))) )
+                
.onFailureOrException(Functionals.function(EntityFunctions.updatingSensorMapEntry(this,
 Attributes.SERVICE_PROBLEMS, "mirror-feed",
+                        Suppliers.ofInstance("error contacting service")
+                    ))))
             .build();
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/software/base/src/main/java/brooklyn/entity/software/MachineLifecycleEffectorTasks.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/main/java/brooklyn/entity/software/MachineLifecycleEffectorTasks.java
 
b/software/base/src/main/java/brooklyn/entity/software/MachineLifecycleEffectorTasks.java
index 3272f3c..b708b61 100644
--- 
a/software/base/src/main/java/brooklyn/entity/software/MachineLifecycleEffectorTasks.java
+++ 
b/software/base/src/main/java/brooklyn/entity/software/MachineLifecycleEffectorTasks.java
@@ -41,6 +41,7 @@ import 
brooklyn.entity.basic.EffectorStartableImpl.StartParameters;
 import brooklyn.entity.basic.Entities;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
 import brooklyn.entity.basic.SoftwareProcess;
 import brooklyn.entity.effector.EffectorBody;
 import brooklyn.entity.effector.Effectors;
@@ -191,16 +192,15 @@ public abstract class MachineLifecycleEffectorTasks {
     
     // ---------------------
     
-    /** runs the tasks needed to start, wrapped by setting {@link 
Attributes#SERVICE_STATE} appropriately */ 
+    /** runs the tasks needed to start, wrapped by setting {@link 
Attributes#SERVICE_STATE_EXPECTED} appropriately */ 
     public void start(Collection<? extends Location> locations) {
-        entity().setAttribute(Attributes.SERVICE_STATE, Lifecycle.STARTING);
+        ServiceStateLogic.setExpectedState(entity(), Lifecycle.STARTING);
         try {
             startInLocations(locations);
             DynamicTasks.waitForLast();
-            if (entity().getAttribute(Attributes.SERVICE_STATE) == 
Lifecycle.STARTING) 
-                entity().setAttribute(Attributes.SERVICE_STATE, 
Lifecycle.RUNNING);
+            ServiceStateLogic.setExpectedState(entity(), Lifecycle.RUNNING);
         } catch (Throwable t) {
-            entity().setAttribute(Attributes.SERVICE_STATE, Lifecycle.ON_FIRE);
+            ServiceStateLogic.setExpectedState(entity(), Lifecycle.ON_FIRE);
             throw Exceptions.propagate(t);
         }
     }
@@ -298,6 +298,7 @@ public abstract class MachineLifecycleEffectorTasks {
             entity().setAttribute(Attributes.HOSTNAME, 
machine.getAddress().getHostName());
             entity().setAttribute(Attributes.ADDRESS, 
machine.getAddress().getHostAddress());
             if (machine instanceof SshMachineLocation) {
+                @SuppressWarnings("resource")
                 SshMachineLocation sshMachine = (SshMachineLocation) machine;
                 UserAndHostAndPort sshAddress = 
UserAndHostAndPort.fromParts(sshMachine.getUser(), 
sshMachine.getAddress().getHostName(), sshMachine.getPort());
                 entity().setAttribute(Attributes.SSH_ADDRESS, sshAddress);
@@ -396,7 +397,7 @@ public abstract class MachineLifecycleEffectorTasks {
     
     /** default restart impl, stops processes if possible, then starts the 
entity again */
     public void restart() {
-        entity().setAttribute(Attributes.SERVICE_STATE, Lifecycle.STOPPING);
+        ServiceStateLogic.setExpectedState(entity(), Lifecycle.STOPPING);
         DynamicTasks.queue("stopping (process)", new Callable<String>() { 
public String call() {
             DynamicTasks.markInessential();
             stopProcessesAtMachine();
@@ -407,11 +408,10 @@ public abstract class MachineLifecycleEffectorTasks {
         DynamicTasks.queue("starting", new Runnable() { public void run() {
             // startInLocations will look up the location, and provision a 
machine if necessary
             // (if it remembered the provisioning location)
-            entity().setAttribute(Attributes.SERVICE_STATE, 
Lifecycle.STARTING);
+            ServiceStateLogic.setExpectedState(entity(), Lifecycle.STARTING);
             startInLocations(null);
             DynamicTasks.waitForLast();
-            if (entity().getAttribute(Attributes.SERVICE_STATE) == 
Lifecycle.STARTING) 
-                entity().setAttribute(Attributes.SERVICE_STATE, 
Lifecycle.RUNNING);
+            ServiceStateLogic.setExpectedState(entity(), Lifecycle.RUNNING);
         }});
     }
 
@@ -424,17 +424,17 @@ public abstract class MachineLifecycleEffectorTasks {
         log.info("Stopping {} in {}", entity(), entity().getLocations());
         
         DynamicTasks.queue("pre-stop", new Callable<String>() { public String 
call() {
-            if 
(entity().getAttribute(SoftwareProcess.SERVICE_STATE)==Lifecycle.STOPPED) {
+            if 
(entity().getAttribute(SoftwareProcess.SERVICE_STATE_ACTUAL)==Lifecycle.STOPPED)
 {
                 log.debug("Skipping stop of entity "+entity()+" when already 
stopped");
                 return "Already stopped";
             }
-            entity().setAttribute(SoftwareProcess.SERVICE_STATE, 
Lifecycle.STOPPING);
+            ServiceStateLogic.setExpectedState(entity(), Lifecycle.STOPPING);
             entity().setAttribute(SoftwareProcess.SERVICE_UP, false);
             preStopCustom();
             return null;
         }});
         
-        if 
(entity().getAttribute(SoftwareProcess.SERVICE_STATE)==Lifecycle.STOPPED) {
+        if 
(entity().getAttribute(SoftwareProcess.SERVICE_STATE_ACTUAL)==Lifecycle.STOPPED)
 {
             return;
         }
                
@@ -448,7 +448,7 @@ public abstract class MachineLifecycleEffectorTasks {
         
         // Release this machine (even if error trying to stop process - we 
rethrow that after)
         Task<StopMachineDetails<Integer>> stoppingMachine = 
DynamicTasks.queue("stopping (machine)", new 
Callable<StopMachineDetails<Integer>>() { public StopMachineDetails<Integer> 
call() {
-            if 
(entity().getAttribute(SoftwareProcess.SERVICE_STATE)==Lifecycle.STOPPED) {
+            if 
(entity().getAttribute(SoftwareProcess.SERVICE_STATE_ACTUAL)==Lifecycle.STOPPED)
 {
                 log.debug("Skipping stop of entity "+entity()+" when already 
stopped");
                 return new StopMachineDetails<Integer>("Already stopped", 0);
             }
@@ -478,9 +478,9 @@ public abstract class MachineLifecycleEffectorTasks {
             }
             
             entity().setAttribute(SoftwareProcess.SERVICE_UP, false);
-            entity().setAttribute(SoftwareProcess.SERVICE_STATE, 
Lifecycle.STOPPED);
+            ServiceStateLogic.setExpectedState(entity(), Lifecycle.STOPPED);
         } catch (Throwable e) {
-            entity().setAttribute(SoftwareProcess.SERVICE_STATE, 
Lifecycle.ON_FIRE);
+            ServiceStateLogic.setExpectedState(entity(), Lifecycle.ON_FIRE);
             Exceptions.propagate(e);
         }
         

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/software/base/src/test/java/brooklyn/entity/basic/lifecycle/ScriptHelperTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/brooklyn/entity/basic/lifecycle/ScriptHelperTest.java
 
b/software/base/src/test/java/brooklyn/entity/basic/lifecycle/ScriptHelperTest.java
index 6edd9a9..daa6109 100644
--- 
a/software/base/src/test/java/brooklyn/entity/basic/lifecycle/ScriptHelperTest.java
+++ 
b/software/base/src/test/java/brooklyn/entity/basic/lifecycle/ScriptHelperTest.java
@@ -117,6 +117,7 @@ public class ScriptHelperTest extends 
BrooklynAppUnitTestSupport {
             FunctionFeed.builder()
                 .entity(this)
                 .period(Duration.millis(10))
+                .onlyIfServiceUp()
                 .poll(new FunctionPollConfig<Boolean, 
Boolean>(SERVICE_PROCESS_IS_RUNNING)
                     .onException(Functions.constant(Boolean.FALSE))
                     .callable(new Callable<Boolean>() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/97eed6bd/software/base/src/test/java/brooklyn/entity/java/VanillaJavaAppTest.java
----------------------------------------------------------------------
diff --git 
a/software/base/src/test/java/brooklyn/entity/java/VanillaJavaAppTest.java 
b/software/base/src/test/java/brooklyn/entity/java/VanillaJavaAppTest.java
index a1347bf..f719882 100644
--- a/software/base/src/test/java/brooklyn/entity/java/VanillaJavaAppTest.java
+++ b/software/base/src/test/java/brooklyn/entity/java/VanillaJavaAppTest.java
@@ -127,10 +127,10 @@ public class VanillaJavaAppTest {
             .configure("main", main).configure("classpath", 
ImmutableList.of(BROOKLYN_THIS_CLASSPATH))
             .configure("args", ImmutableList.of()));
         app.start(ImmutableList.of(loc));
-        assertEquals(javaProcess.getAttribute(VanillaJavaApp.SERVICE_STATE), 
Lifecycle.RUNNING);
+        
assertEquals(javaProcess.getAttribute(VanillaJavaApp.SERVICE_STATE_ACTUAL), 
Lifecycle.RUNNING);
 
         javaProcess.stop();
-        assertEquals(javaProcess.getAttribute(VanillaJavaApp.SERVICE_STATE), 
Lifecycle.STOPPED);
+        
assertEquals(javaProcess.getAttribute(VanillaJavaApp.SERVICE_STATE_ACTUAL), 
Lifecycle.STOPPED);
     }
 
     @Test(groups={"Integration"})

Reply via email to