simplify yaml for averaging + summing enricher, plus test
Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/5af29257 Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/5af29257 Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/5af29257 Branch: refs/heads/master Commit: 5af2925722042561838e12b0d5902d2852eef431 Parents: ff31a41 Author: Alex Heneveld <[email protected]> Authored: Sat Apr 18 16:45:22 2015 +0100 Committer: Alex Heneveld <[email protected]> Committed: Sat Apr 18 17:21:23 2015 +0100 ---------------------------------------------------------------------- .../main/java/brooklyn/enricher/Enrichers.java | 89 ++++++++++++++++++-- .../brooklyn/enricher/basic/Aggregator.java | 34 ++++++++ .../EnrichersSlightlySimplerYamlTest.java | 38 +++++++++ .../test-webapp-with-averaging-enricher.yaml | 47 +++++++++++ .../java/brooklyn/util/math/MathPredicates.java | 32 +++++++ 5 files changed, 231 insertions(+), 9 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5af29257/core/src/main/java/brooklyn/enricher/Enrichers.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/brooklyn/enricher/Enrichers.java b/core/src/main/java/brooklyn/enricher/Enrichers.java index 9b34b13..c8ee899 100644 --- a/core/src/main/java/brooklyn/enricher/Enrichers.java +++ b/core/src/main/java/brooklyn/enricher/Enrichers.java @@ -46,11 +46,13 @@ import brooklyn.util.flags.TypeCoercions; import brooklyn.util.text.StringPredicates; import brooklyn.util.text.Strings; +import com.google.common.annotations.Beta; import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; -import com.google.common.base.Predicates; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; @@ -181,7 +183,8 @@ public class Enrichers { protected final AttributeSensor<S> aggregating; protected AttributeSensor<T> publishing; protected Entity fromEntity; - protected Function<? super Collection<S>, ? extends T> computing; + // use supplier so latest values of other fields can be used + protected Supplier<Function<? super Collection<S>, ? extends T>> computingSupplier; protected Boolean fromMembers; protected Boolean fromChildren; protected Boolean excludingBlank; @@ -216,13 +219,25 @@ public class Enrichers { this.fromHardcodedProducers = ImmutableSet.copyOf(val); return self(); } + @SuppressWarnings({ "unchecked", "rawtypes" }) public B computing(Function<? super Collection<S>, ? extends T> val) { - this.computing = checkNotNull(val); + this.computingSupplier = (Supplier)Suppliers.ofInstance(checkNotNull(val)); return self(); } - @SuppressWarnings({ "unchecked", "rawtypes" }) public B computingSum() { - // relies of TypeCoercion of result from Number to T, and type erasure for us to get away with it! + this.computingSupplier = new Supplier<Function<? super Collection<S>, ? extends T>>() { + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Function<? super Collection<S>, ? extends T> get() { + // relies on TypeCoercion of result from Number to T, and type erasure for us to get away with it! + return (Function)new ComputingSum((Number)defaultValueForUnreportedSensors, (Number)valueToReportIfNoSensors, publishing.getTypeToken()); + } + }; + return self(); + } + @SuppressWarnings({ "unchecked", "rawtypes", "unused" }) + private B computingSumLegacy() { + // since 0.7.0, kept in case we need to rebind to this Function<Collection<S>, Number> function = new Function<Collection<S>, Number>() { @Override public Number apply(Collection<S> input) { return sum((Collection)input, (Number)defaultValueForUnreportedSensors, (Number)valueToReportIfNoSensors, (TypeToken) publishing.getTypeToken()); @@ -230,9 +245,21 @@ public class Enrichers { this.computing((Function)function); return self(); } - @SuppressWarnings({ "unchecked", "rawtypes" }) + public B computingAverage() { - // relies of TypeCoercion of result from Number to T, and type erasure for us to get away with it! + this.computingSupplier = new Supplier<Function<? super Collection<S>, ? extends T>>() { + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Function<? super Collection<S>, ? extends T> get() { + // relies on TypeCoercion of result from Number to T, and type erasure for us to get away with it! + return (Function)new ComputingAverage((Number)defaultValueForUnreportedSensors, (Number)valueToReportIfNoSensors, publishing.getTypeToken()); + } + }; + return self(); + } + @SuppressWarnings({ "unchecked", "rawtypes", "unused" }) + private B computingAverageLegacy() { + // since 0.7.0, kept in case we need to rebind to this Function<Collection<S>, Number> function = new Function<Collection<S>, Number>() { @Override public Number apply(Collection<S> input) { return average((Collection)input, (Number)defaultValueForUnreportedSensors, (Number)valueToReportIfNoSensors, (TypeToken) publishing.getTypeToken()); @@ -240,6 +267,7 @@ public class Enrichers { this.computing((Function)function); return self(); } + public B defaultValueForUnreportedSensors(S val) { this.defaultValueForUnreportedSensors = val; return self(); @@ -282,7 +310,7 @@ public class Enrichers { .put(Aggregator.SOURCE_SENSOR, aggregating) .putIfNotNull(Aggregator.FROM_CHILDREN, fromChildren) .putIfNotNull(Aggregator.FROM_MEMBERS, fromMembers) - .putIfNotNull(Aggregator.TRANSFORMATION, computing) + .putIfNotNull(Aggregator.TRANSFORMATION, computingSupplier.get()) .putIfNotNull(Aggregator.FROM_HARDCODED_PRODUCERS, fromHardcodedProducers) .putIfNotNull(Aggregator.ENTITY_FILTER, entityFilter) .putIfNotNull(Aggregator.VALUE_FILTER, valueFilter) @@ -297,7 +325,7 @@ public class Enrichers { .add("aggregating", aggregating) .add("publishing", publishing) .add("fromEntity", fromEntity) - .add("computing", computing) + .add("computing", computingSupplier) .add("fromMembers", fromMembers) .add("fromChildren", fromChildren) .add("excludingBlank", excludingBlank) @@ -703,6 +731,49 @@ public class Enrichers { } } + @Beta + private abstract static class ComputingNumber<T extends Number> implements Function<Collection<T>, T> { + protected final Number defaultValueForUnreportedSensors; + protected final Number valueToReportIfNoSensors; + protected final TypeToken<T> typeToken; + @SuppressWarnings({ "rawtypes", "unchecked" }) + public ComputingNumber(Number defaultValueForUnreportedSensors, Number valueToReportIfNoSensors, TypeToken<T> typeToken) { + this.defaultValueForUnreportedSensors = defaultValueForUnreportedSensors; + this.valueToReportIfNoSensors = valueToReportIfNoSensors; + if (typeToken!=null && TypeToken.of(Number.class).isAssignableFrom(typeToken.getType())) { + this.typeToken = typeToken; + } else if (typeToken==null || typeToken.isAssignableFrom(Number.class)) { + // use double if e.g. Object is supplied + this.typeToken = (TypeToken)TypeToken.of(Double.class); + } else { + throw new IllegalArgumentException("Type "+typeToken+" is not valid for "+this); + } + } + @Override public abstract T apply(Collection<T> input); + } + + @Beta + public static class ComputingSum<T extends Number> extends ComputingNumber<T> { + public ComputingSum(Number defaultValueForUnreportedSensors, Number valueToReportIfNoSensors, TypeToken<T> typeToken) { + super(defaultValueForUnreportedSensors, valueToReportIfNoSensors, typeToken); + } + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override public T apply(Collection<T> input) { + return (T) sum((Collection)input, (Number)defaultValueForUnreportedSensors, (Number)valueToReportIfNoSensors, typeToken); + } + } + + @Beta + public static class ComputingAverage<T extends Number> extends ComputingNumber<T> { + public ComputingAverage(Number defaultValueForUnreportedSensors, Number valueToReportIfNoSensors, TypeToken<T> typeToken) { + super(defaultValueForUnreportedSensors, valueToReportIfNoSensors, typeToken); + } + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override public T apply(Collection<T> input) { + return (T) average((Collection)input, (Number)defaultValueForUnreportedSensors, (Number)valueToReportIfNoSensors, typeToken); + } + } + protected static <T extends Number> T average(Collection<T> vals, Number defaultValueForUnreportedSensors, Number valueToReportIfNoSensors, TypeToken<T> type) { Double doubleValueToReportIfNoSensors = (valueToReportIfNoSensors == null) ? null : valueToReportIfNoSensors.doubleValue(); int count = count(vals, defaultValueForUnreportedSensors!=null); http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5af29257/core/src/main/java/brooklyn/enricher/basic/Aggregator.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/brooklyn/enricher/basic/Aggregator.java b/core/src/main/java/brooklyn/enricher/basic/Aggregator.java index cb80431..d7f4fca 100644 --- a/core/src/main/java/brooklyn/enricher/basic/Aggregator.java +++ b/core/src/main/java/brooklyn/enricher/basic/Aggregator.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; import brooklyn.config.BrooklynLogging; import brooklyn.config.ConfigKey; +import brooklyn.enricher.Enrichers; import brooklyn.entity.Entity; import brooklyn.entity.basic.ConfigKeys; import brooklyn.event.AttributeSensor; @@ -38,6 +39,7 @@ import brooklyn.event.SensorEventListener; import brooklyn.util.collections.MutableList; import brooklyn.util.collections.MutableMap; import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.flags.SetFromFlag; import brooklyn.util.text.StringPredicates; import com.google.common.base.Function; @@ -54,7 +56,13 @@ public class Aggregator<T,U> extends AbstractAggregator<T,U> implements SensorEv private static final Logger LOG = LoggerFactory.getLogger(Aggregator.class); public static final ConfigKey<Sensor<?>> SOURCE_SENSOR = ConfigKeys.newConfigKey(new TypeToken<Sensor<?>>() {}, "enricher.sourceSensor"); + + @SetFromFlag("transformation") + public static final ConfigKey<Object> TRANSFORMATION_UNTYPED = ConfigKeys.newConfigKey(Object.class, "enricher.transformation.untyped", + "Specifies a transformation, as a function from a collection to the value, or as a string matching a pre-defined named transformation, " + + "such as 'average' (for numbers), 'add' (for numbers), or 'list' (the default, putting any collection of items into a list)"); public static final ConfigKey<Function<? super Collection<?>, ?>> TRANSFORMATION = ConfigKeys.newConfigKey(new TypeToken<Function<? super Collection<?>, ?>>() {}, "enricher.transformation"); + public static final ConfigKey<Boolean> EXCLUDE_BLANK = ConfigKeys.newBooleanConfigKey("enricher.aggregator.excludeBlank", "Whether explicit nulls or blank strings should be excluded (default false); this only applies if no value filter set", false); protected Sensor<T> sourceSensor; @@ -73,9 +81,35 @@ public class Aggregator<T,U> extends AbstractAggregator<T,U> implements SensorEv protected void setEntityLoadingConfig() { super.setEntityLoadingConfig(); this.sourceSensor = (Sensor<T>) getRequiredConfig(SOURCE_SENSOR); + + Object t1 = config().get(TRANSFORMATION_UNTYPED); + if (t1 instanceof String) t1 = lookupTransformation((String)t1); + this.transformation = (Function<? super Collection<T>, ? extends U>) config().get(TRANSFORMATION); + if (this.transformation==null) { + this.transformation = (Function<? super Collection<T>, ? extends U>) t1; + } else if (t1!=null && !t1.equals(this.transformation)) { + throw new IllegalStateException("Cannot supply both "+TRANSFORMATION_UNTYPED+" and "+TRANSFORMATION+" unless they are equal."); + } } + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected Function<? super Collection<?>, ?> lookupTransformation(String t1) { + if ("average".equalsIgnoreCase(t1)) return new Enrichers.ComputingAverage(null, null, targetSensor.getTypeToken()); + if ("sum".equalsIgnoreCase(t1)) return new Enrichers.ComputingAverage(null, null, targetSensor.getTypeToken()); + if ("list".equalsIgnoreCase(t1)) return new ComputingList(); + return null; + } + + private class ComputingList<TT> implements Function<Collection<TT>, List<TT>> { + @Override + public List<TT> apply(Collection<TT> input) { + if (input==null) return null; + return MutableList.copyOf(input).asUnmodifiable(); + } + + } + @Override protected void setEntityBeforeSubscribingProducerChildrenEvents() { BrooklynLogging.log(LOG, BrooklynLogging.levelDebugOrTraceIfReadOnly(producer), http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5af29257/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/EnrichersSlightlySimplerYamlTest.java ---------------------------------------------------------------------- diff --git a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/EnrichersSlightlySimplerYamlTest.java b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/EnrichersSlightlySimplerYamlTest.java index 99165c5..9001824 100644 --- a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/EnrichersSlightlySimplerYamlTest.java +++ b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/EnrichersSlightlySimplerYamlTest.java @@ -21,6 +21,7 @@ package io.brooklyn.camp.brooklyn; import java.net.URI; import java.util.Collection; import java.util.Iterator; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,9 +33,12 @@ import brooklyn.entity.basic.Attributes; import brooklyn.entity.basic.Entities; import brooklyn.entity.basic.EntityInternal; import brooklyn.entity.group.DynamicCluster; +import brooklyn.entity.webapp.JavaWebAppSoftwareProcess; import brooklyn.event.basic.Sensors; import brooklyn.test.EntityTestUtils; import brooklyn.util.collections.CollectionFunctionals; +import brooklyn.util.collections.MutableList; +import brooklyn.util.math.MathPredicates; import brooklyn.util.text.StringPredicates; import com.google.common.base.Predicate; @@ -88,6 +92,40 @@ public class EnrichersSlightlySimplerYamlTest extends AbstractYamlTest { // TODO would we want to allow "all-but-usual" as the default if nothing specified } + @Test(groups="Integration") + public void testWebappWithAveragingEnricher() throws Exception { + Entity app = createAndStartApplication(loadYaml("test-webapp-with-averaging-enricher.yaml")); + waitForApplicationTasks(app); + log.info("Started "+app+":"); + Entities.dumpInfo(app); + + List<JavaWebAppSoftwareProcess> appservers = MutableList.copyOf(Entities.descendants(app, JavaWebAppSoftwareProcess.class)); + Assert.assertEquals(appservers.size(), 3); + + EntityInternal srv0 = (EntityInternal) appservers.get(0); + EntityInternal dwac = (EntityInternal) srv0.getParent(); + EntityInternal cdwac = (EntityInternal) dwac.getParent(); + + srv0.setAttribute(Sensors.newDoubleSensor("my.load"), 20.0); + + EntityTestUtils.assertAttributeEventually(dwac, Sensors.newSensor(Double.class, "my.load.averaged"), + MathPredicates.equalsApproximately(20)); + EntityTestUtils.assertAttributeEventually(cdwac, Sensors.newSensor(Double.class, "my.load.averaged"), + MathPredicates.equalsApproximately(20)); + + srv0.setAttribute(Sensors.newDoubleSensor("my.load"), null); + EntityTestUtils.assertAttributeEventually(cdwac, Sensors.newSensor(Double.class, "my.load.averaged"), + Predicates.isNull()); + + ((EntityInternal) appservers.get(1)).setAttribute(Sensors.newDoubleSensor("my.load"), 10.0); + ((EntityInternal) appservers.get(2)).setAttribute(Sensors.newDoubleSensor("my.load"), 20.0); + EntityTestUtils.assertAttributeEventually(cdwac, Sensors.newSensor(Double.class, "my.load.averaged"), + MathPredicates.equalsApproximately(15)); + srv0.setAttribute(Sensors.newDoubleSensor("my.load"), 0.0); + EntityTestUtils.assertAttributeEventually(cdwac, Sensors.newSensor(Double.class, "my.load.averaged"), + MathPredicates.equalsApproximately(10)); + } + @Override protected Logger getLogger() { return log; http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5af29257/usage/camp/src/test/resources/test-webapp-with-averaging-enricher.yaml ---------------------------------------------------------------------- diff --git a/usage/camp/src/test/resources/test-webapp-with-averaging-enricher.yaml b/usage/camp/src/test/resources/test-webapp-with-averaging-enricher.yaml new file mode 100644 index 0000000..d4fc6ee --- /dev/null +++ b/usage/camp/src/test/resources/test-webapp-with-averaging-enricher.yaml @@ -0,0 +1,47 @@ +# +# 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. +# +# example showing how enrichers can be set +# +name: test-webapp-with-averaging-enricher +description: Testing many enrichers +services: +- type: brooklyn.entity.webapp.ControlledDynamicWebAppCluster + initialSize: 3 + location: localhost + + # define the web cluster, adding an averaging enricher to the cluster. + # this assumes the test fixture will set the "my.load" sensor on the member-specs in here. + webClusterSpec: + $brooklyn:entitySpec: + type: brooklyn.entity.webapp.DynamicWebAppCluster + id: cluster + brooklyn.enrichers: + - type: brooklyn.enricher.basic.Aggregator + brooklyn.config: + enricher.sourceSensor: $brooklyn:sensor("my.load") + enricher.targetSensor: $brooklyn:sensor("my.load.averaged") + enricher.aggregating.fromMembers: true + transformation: average + + brooklyn.enrichers: + - type: brooklyn.enricher.basic.Propagator + brooklyn.config: + producer: $brooklyn:entity("cluster") + propagating: + - $brooklyn:sensor("my.load.averaged") http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5af29257/utils/common/src/main/java/brooklyn/util/math/MathPredicates.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/brooklyn/util/math/MathPredicates.java b/utils/common/src/main/java/brooklyn/util/math/MathPredicates.java index 7a5b325..41f4024 100644 --- a/utils/common/src/main/java/brooklyn/util/math/MathPredicates.java +++ b/utils/common/src/main/java/brooklyn/util/math/MathPredicates.java @@ -20,6 +20,7 @@ package brooklyn.util.math; import javax.annotation.Nullable; +import com.google.common.base.Preconditions; import com.google.common.base.Predicate; public class MathPredicates { @@ -71,4 +72,35 @@ public class MathPredicates { } }; } + + /** + * Creates a predicate comparing a given number with {@code val}. + * A number of {@code null} passed to the predicate will always return false. + */ + public static <T extends Number> Predicate<T> equalsApproximately(final Number val, final double delta) { + return new EqualsApproximately<T>(val, delta); + } + /** Convenience for {@link #equalsApproximately(double,double)} with a delta of 10^{-6}. */ + public static <T extends Number> Predicate<T> equalsApproximately(final Number val) { + return equalsApproximately(val, 0.0000001); + } + + private static final class EqualsApproximately<T extends Number> implements Predicate<T> { + private final double val; + private final double delta; + private EqualsApproximately(Number val, double delta) { + this.val = val.doubleValue(); + Preconditions.checkArgument(delta>=0, "delta must be non-negative"); + this.delta = delta; + } + public boolean apply(@Nullable T input) { + return (input == null) ? false : Math.abs(input.doubleValue() - val) <= delta; + } + @Override + public String toString() { + return "equals-approximately("+val+" +- "+delta+")"; + } + } + + }
