diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java index 2836895d16..62ae840da4 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java @@ -70,6 +70,7 @@ import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -233,6 +234,18 @@ public static BrooklynDslDeferredSupplier entityId() { return new DslComponent(Scope.THIS, "").entityId(); } + public static BrooklynDslDeferredSupplier effector(String effectorName, Map args) { + return new DslComponent(Scope.THIS, "").effector(effectorName, args); + } + + public static BrooklynDslDeferredSupplier effector(String effectorName) { + return new DslComponent(Scope.THIS, "").effector(effectorName, ImmutableMap.of()); + } + + public static BrooklynDslDeferredSupplier effector(String effectorName, Object... args) { + return new DslComponent(Scope.THIS, "").effector(effectorName, args); + } + /** Returns a {@link Sensor}, looking up the sensor on the context if available and using that, * or else defining an untyped (Object) sensor */ @DslAccessible diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java index 9de33405b7..a5d9fcd869 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java @@ -20,13 +20,19 @@ import static org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils.resolved; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import org.apache.brooklyn.api.effector.Effector; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.mgmt.ExecutionContext; import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.api.mgmt.TaskAdaptable; +import org.apache.brooklyn.api.mgmt.TaskFactory; import org.apache.brooklyn.api.objs.BrooklynObject; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.api.sensor.Sensor; @@ -43,25 +49,32 @@ import org.apache.brooklyn.core.mgmt.internal.EntityManagerInternal; import org.apache.brooklyn.core.sensor.DependentConfiguration; import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.flags.TypeCoercions; import org.apache.brooklyn.util.core.task.BasicExecutionContext; import org.apache.brooklyn.util.core.task.DeferredSupplier; +import org.apache.brooklyn.util.core.task.HasSideEffects; import org.apache.brooklyn.util.core.task.ImmediateSupplier; import org.apache.brooklyn.util.core.task.TaskBuilder; import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.groovy.GroovyJavaMethods; import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes; import org.apache.brooklyn.util.text.Strings; import com.google.common.base.CaseFormat; import com.google.common.base.Converter; +import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.google.common.util.concurrent.Callables; public class DslComponent extends BrooklynDslDeferredSupplier implements DslFunctionSource { @@ -509,6 +522,119 @@ public String toString() { } } + @DslAccessible + public BrooklynDslDeferredSupplier effector(final String effectorName) { + return new ExecuteEffector(this, effectorName, ImmutableMap.of()); + } + @DslAccessible + public BrooklynDslDeferredSupplier effector(final String effectorName, final Map args) { + return new ExecuteEffector(this, effectorName, args); + } + public BrooklynDslDeferredSupplier effector(final String effectorName, Object... args) { + return new ExecuteEffector(this, effectorName, ImmutableList.copyOf(args)); + } + protected static class ExecuteEffector extends BrooklynDslDeferredSupplier implements HasSideEffects { + private static final long serialVersionUID = 1740899524088902383L; + private final DslComponent component; + private final String effectorName; + private final Map args; + private final List argList; + private Task cachedTask; + + public ExecuteEffector(DslComponent component, String effectorName, Map args) { + this.component = Preconditions.checkNotNull(component); + this.effectorName = effectorName; + this.args = args; + this.argList = null; + } + + public ExecuteEffector(DslComponent component, String effectorName, List args) { + this.component = Preconditions.checkNotNull(component); + this.effectorName = effectorName; + this.argList = args; + this.args = null; + } + + @Override + public Maybe getImmediately() { + return Maybe.absent(); + } + + @SuppressWarnings("unchecked") + @Override + public Task newTask() { + Entity targetEntity = component.get(); + Maybe> targetEffector = targetEntity.getEntityType().getEffectorByName(effectorName); + if (targetEffector.isAbsentOrNull()) { + throw new IllegalArgumentException("Effector " + effectorName + " not found on entity: " + targetEntity); + } + synchronized (this) { + if (cachedTask == null) { + if (argList == null) { + cachedTask = Entities.invokeEffector(targetEntity, targetEntity, targetEffector.get(), args); + } else { + cachedTask = invokeWithDeferredArgs(targetEntity, targetEffector.get(), argList); + } + } + } + return (Task) cachedTask; + } + + private Task invokeWithDeferredArgs(final Entity targetEntity, final Effector targetEffector, final List args) { + List> taskArgs = Lists.newArrayList(); + for (Object arg : args) { + if (arg instanceof TaskAdaptable) { + taskArgs.add((TaskAdaptable) arg); + } else if (arg instanceof TaskFactory) { + taskArgs.add(((TaskFactory>) arg).newTask()); + } + } + + return DependentConfiguration.transformMultiple( + MutableMap.of("displayName", "invoking '"+targetEffector.getName()+"' with "+taskArgs.size()+" task"+(taskArgs.size()!=1?"s":"")), + new Function, Object>() { + @Override + public Object apply(List input) { + Iterator tri = input.iterator(); + Object[] vv = new Object[args.size()]; + int i=0; + for (Object arg : args) { + if (arg instanceof TaskAdaptable || arg instanceof TaskFactory) { + vv[i] = tri.next(); + } else if (arg instanceof DeferredSupplier) { + vv[i] = ((DeferredSupplier) arg).get(); + } else { + vv[i] = arg; + } + i++; + } + return Entities.invokeEffectorWithArgs(targetEntity, targetEntity, targetEffector, vv); + } + }, + taskArgs); + } + + @Override + public int hashCode() { + return Objects.hashCode(component, effectorName); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + ExecuteEffector that = ExecuteEffector.class.cast(obj); + return Objects.equal(this.component, that.component) && + Objects.equal(this.effectorName, that.effectorName); + } + + @Override + public String toString() { + return (component.scope==Scope.THIS ? "" : component.toString()+".") + + "effector("+JavaStringEscapes.wrapJavaString(effectorName)+")"; + } + } + @DslAccessible public BrooklynDslDeferredSupplier config(final Object keyNameOrSupplier) { return new DslConfigSupplier(this, keyNameOrSupplier); @@ -763,5 +889,5 @@ public String toString() { return DslToStringHelpers.component(scopeComponent, remainder); } - + } diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/EffectorsYamlIntegrationTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/EffectorsYamlIntegrationTest.java new file mode 100644 index 0000000000..be4cdb50de --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/EffectorsYamlIntegrationTest.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.brooklyn; + +import com.google.common.collect.Iterables; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.entity.software.base.SameServerEntity; +import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.apache.brooklyn.core.entity.EntityPredicates.displayNameEqualTo; +import static org.testng.Assert.assertEquals; + +@Test +public class EffectorsYamlIntegrationTest extends AbstractYamlTest { + private static final Logger log = LoggerFactory.getLogger(EffectorsYamlIntegrationTest.class); + + @Test(groups = "Integration") + public void testInteractingWithAnotherEntityForStartup() throws Exception { + + final Path tempFile = Files.createTempFile("testInteractingWithAnotherEntityForStartup", ".txt"); + getLogger().info("Temp file is {}", tempFile.toAbsolutePath()); + + try { + Entity app = createAndStartApplication( + "location: localhost:(name=localhost)", + "services:", + "- type: " + SameServerEntity.class.getName(), + " brooklyn.children:", + " - type: " + TestEntity.class.getName(), + " id: testEntity", + " name: testEntity", + " brooklyn.initializers:", + " - type: org.apache.brooklyn.core.sensor.ssh.SshCommandSensor", + " brooklyn.config:", + " name: greeting", + " period: 2s", + " command: |", + " echo hello world", + " - type: " + VanillaSoftwareProcess.class.getName(), + " id: consumerEntity", + " brooklyn.config:", + " install.latch: $brooklyn:entity(\"testEntity\").attributeWhenReady(\"service.isUp\")", + " launch.command: while true; do sleep 3600 ; done & echo $! > ${PID_FILE}", + " shell.env:", + " RESPONSE: $brooklyn:entity(\"testEntity\").effector(\"identityEffector\", $brooklyn:entity(\"testEntity\").attributeWhenReady(\"greeting\"))", + " post.launch.command: echo ${RESPONSE} > " + tempFile.toAbsolutePath() + ); + waitForApplicationTasks(app); + + final String contents = new String(Files.readAllBytes(tempFile)).trim(); + assertEquals(contents, "hello world", "file contents: " + contents); + + } finally { + Files.delete(tempFile); + } + } + + + private void assertCallHistory(TestEntity testEntity, String... expectedCalls) { + List callHistory = testEntity.getCallHistory(); + Assert.assertEquals(callHistory.size(), expectedCalls.length, "history = " + callHistory); + int c = 0; + for (String expected : expectedCalls) { + Assert.assertEquals(callHistory.get(c++), expected, "history = " + callHistory); + } + } + + @Override + protected Logger getLogger() { + return log; + } + +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/EffectorsYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/EffectorsYamlTest.java new file mode 100644 index 0000000000..4d3078b8f9 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/EffectorsYamlTest.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.brooklyn; + +import static com.google.common.collect.Iterables.filter; +import static org.apache.brooklyn.core.entity.EntityPredicates.displayNameEqualTo; +import static org.testng.Assert.assertEquals; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.collect.Iterables; + +@Test +public class EffectorsYamlTest extends AbstractYamlTest { + private static final Logger log = LoggerFactory.getLogger(EffectorsYamlTest.class); + + @Test + public void testEffectorWithVoidReturnInvokedByGetConfig() throws Exception { + Entity app = createAndStartApplication( + "services:", + "- type: " + TestEntity.class.getName(), + " id: entity1", + " brooklyn.config:", + " test.confName: $brooklyn:entity(\"entity1\").effector(\"myEffector\")" + ); + TestEntity testEntity = (TestEntity)Iterables.getOnlyElement(app.getChildren()); + + assertCallHistory(testEntity, "start"); + + // invoke the effector + Assert.assertNull(testEntity.getConfig(TestEntity.CONF_NAME)); + + assertCallHistory(testEntity, "start", "myEffector"); + } + + + @Test(enabled = false, description = "currently not possible to say '$brooklyn:entity(\"entity1\").effector:'") + public void testEffectorMultiLine() throws Exception { + Entity app = createAndStartApplication( + "services:", + "- type: " + TestEntity.class.getName(), + " id: entity1", + " brooklyn.config:", + " test.confName: ", + " $brooklyn:entity(\"entity1\").effector:", + " - myEffector" + ); + TestEntity testEntity = (TestEntity)Iterables.getOnlyElement(app.getChildren()); + + assertCallHistory(testEntity, "start"); + + Assert.assertNull(testEntity.getConfig(TestEntity.CONF_NAME)); + + assertCallHistory(testEntity, "start", "myEffector"); + } + + @Test + public void testEntityEffectorWithReturn() throws Exception { + Entity app = createAndStartApplication( + "services:", + "- type: " + TestEntity.class.getName(), + " id: entity1", + " brooklyn.config:", + " test.confName: ", + " $brooklyn:entity(\"entity1\").effector(\"identityEffector\", \"hello\")" + ); + TestEntity testEntity = (TestEntity)Iterables.getOnlyElement(app.getChildren()); + Assert.assertEquals(testEntity.getConfig(TestEntity.CONF_NAME), "hello"); + assertCallHistory(testEntity, "start", "identityEffector"); + } + + @Test + public void testOwnEffectorWithReturn() throws Exception { + Entity app = createAndStartApplication( + "services:", + "- type: " + TestEntity.class.getName(), + " brooklyn.config:", + " test.confName: ", + " $brooklyn:effector(\"identityEffector\", \"my own effector\")" + ); + TestEntity testEntity = (TestEntity)Iterables.getOnlyElement(app.getChildren()); + Assert.assertEquals(testEntity.getConfig(TestEntity.CONF_NAME), "my own effector"); + assertCallHistory(testEntity, "start", "identityEffector"); + } + + @Test + public void testEffectorOnOtherEntityWithReturn() throws Exception { + Entity app = createAndStartApplication( + "services:", + "- type: " + TestEntity.class.getName(), + " id: entityOne", + " name: entityOne", + "- type: " + TestEntity.class.getName(), + " id: entityTwo", + " name: entityTwo", + " brooklyn.config:", + " test.confName: ", + " $brooklyn:entity(\"entityOne\").effector(\"identityEffector\", \"entityOne effector\")" + ); + TestEntity entityOne = (TestEntity) filter(app.getChildren(), displayNameEqualTo("entityOne")).iterator().next(); + TestEntity entityTwo = (TestEntity) filter(app.getChildren(), displayNameEqualTo("entityTwo")).iterator().next(); + Assert.assertEquals(entityTwo.getConfig(TestEntity.CONF_NAME), "entityOne effector"); + assertCallHistory(entityOne, "start", "identityEffector"); + } + + @Test + public void testEffectorCalledOncePerConfigKey() throws Exception { + Entity app = createAndStartApplication( + "services:", + "- type: " + TestEntity.class.getName(), + " id: entity1", + " brooklyn.config:", + " test.confName: ", + " $brooklyn:effector(\"sequenceEffector\")", + " test.confObject: ", + " $brooklyn:effector(\"sequenceEffector\")" + ); + TestEntity testEntity = (TestEntity)Iterables.getOnlyElement(app.getChildren()); + + List callHistory = testEntity.getCallHistory(); + Assert.assertFalse(callHistory.contains("myEffector"), "history = " + callHistory); + + final String firstGetConfig = testEntity.getConfig(TestEntity.CONF_NAME); + Assert.assertEquals(firstGetConfig, "1"); + final String secondGetConfig = testEntity.getConfig(TestEntity.CONF_NAME); + Assert.assertEquals(secondGetConfig, "1"); + Assert.assertEquals(testEntity.getConfig(TestEntity.CONF_OBJECT), Integer.valueOf(2)); + assertCallHistory(testEntity, "start", "sequenceEffector", "sequenceEffector"); + } + + @Test(groups = "Integration") + public void testSshCommandSensorWithEffectorInEnv() throws Exception { + final Path tempFile = Files.createTempFile("testSshCommandSensorWithEffectorInEnv", ".txt"); + getLogger().info("Temp file is {}", tempFile.toAbsolutePath()); + + try { + Entity app = createAndStartApplication( + "location: localhost:(name=localhost)", + "services:", + "- type: " + TestEntity.class.getName(), + " id: testEnt1", + " name: testEnt1", + "- type: " + VanillaSoftwareProcess.class.getName(), + " id: vsp", + " brooklyn.config:", + " launch.command: echo ${MY_ENV_VAR} > " + tempFile.toAbsolutePath(), + " checkRunning.command: true", + " shell.env:", + " MY_ENV_VAR:" , + " $brooklyn:entity(\"testEnt1\").effector(\"identityEffector\", \"from effector\")" + ); + waitForApplicationTasks(app); + + final TestEntity testEnt1 = + (TestEntity) Iterables.filter(app.getChildren(), displayNameEqualTo("testEnt1")).iterator().next(); + assertCallHistory(testEnt1, "start", "identityEffector"); + final String contents = new String(Files.readAllBytes(tempFile)).trim(); + assertEquals(contents, "from effector", "file contents: " + contents); + + } finally { + Files.delete(tempFile); + } + } + + public static void assertCallHistory(TestEntity testEntity, String... expectedCalls) { + List callHistory = testEntity.getCallHistory(); + Assert.assertEquals(callHistory.size(), expectedCalls.length, "history = " + callHistory); + int c = 0; + for (String expected : expectedCalls) { + Assert.assertEquals(callHistory.get(c++), expected, "history = " + callHistory); + } + } + + @Override + protected Logger getLogger() { + return log; + } +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/FormatStringIntegrationTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/FormatStringIntegrationTest.java new file mode 100644 index 0000000000..8e9fb4c159 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/FormatStringIntegrationTest.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.brooklyn.spi.dsl.methods; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest; +import org.apache.brooklyn.entity.software.base.SameServerEntity; +import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.testng.Assert.assertEquals; + +public class FormatStringIntegrationTest extends AbstractYamlTest { + + private static final Logger LOG = LoggerFactory.getLogger(FormatStringIntegrationTest.class); + + @Test(groups = "Integration") + public void testFormatString() throws Exception { + + final Path tempFile = Files.createTempFile("testFormatString", ".txt"); + LOG.info("Temp file is {}", tempFile.toAbsolutePath()); + + try { + Entity app = createAndStartApplication( + "location: localhost:(name=localhost)", + "services:", + "- type: " + SameServerEntity.class.getName(), + " brooklyn.children:", + " - type: " + VanillaSoftwareProcess.class.getName(), + " id: sensorEntity", + " launch.command: while true; do sleep 3600 ; done & echo $! > ${PID_FILE}", + " brooklyn.initializers:", + " - type: org.apache.brooklyn.core.sensor.ssh.SshCommandSensor", + " brooklyn.config:", + " name: greeting", + " period: 2s", + " command: echo world", + " - type: " + VanillaSoftwareProcess.class.getName(), + " id: consumerEntity", + " launch.command: while true; do sleep 3600 ; done & echo $! > ${PID_FILE}", + " shell.env:", + " RESPONSE: $brooklyn:formatString(\"hello %s\", $brooklyn:entity(\"sensorEntity\").attributeWhenReady(\"greeting\"))", + " post.launch.command: echo ${RESPONSE} > " + tempFile.toAbsolutePath() + ); + waitForApplicationTasks(app); + + final String contents = new String(Files.readAllBytes(tempFile)).trim(); + assertEquals(contents, "hello world", "file contents: " + contents); + + } finally { + Files.delete(tempFile); + } + } +} diff --git a/core/src/main/java/org/apache/brooklyn/core/objs/AbstractConfigurationSupportInternal.java b/core/src/main/java/org/apache/brooklyn/core/objs/AbstractConfigurationSupportInternal.java index ce10c86957..4ffd4c41f4 100644 --- a/core/src/main/java/org/apache/brooklyn/core/objs/AbstractConfigurationSupportInternal.java +++ b/core/src/main/java/org/apache/brooklyn/core/objs/AbstractConfigurationSupportInternal.java @@ -147,6 +147,7 @@ protected Maybe getNonBlockingResolvingSimple(ConfigKey key) { .deep(true) .context(getContext()) .swallowExceptions() + .avoidSideEffects() .get(); return (resolved != marker) ? TypeCoercions.tryCoerce(resolved, key.getTypeToken()) diff --git a/core/src/main/java/org/apache/brooklyn/util/core/task/HasSideEffects.java b/core/src/main/java/org/apache/brooklyn/util/core/task/HasSideEffects.java new file mode 100644 index 0000000000..5d3e6a79f3 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/util/core/task/HasSideEffects.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.core.task; + +/** + * Marker interface to indicate that something, e.g. a DeferredSupplier has side effects. + */ +public interface HasSideEffects { +} diff --git a/core/src/main/java/org/apache/brooklyn/util/core/task/ValueResolver.java b/core/src/main/java/org/apache/brooklyn/util/core/task/ValueResolver.java index f81594eba8..afff971cf3 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/task/ValueResolver.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/task/ValueResolver.java @@ -112,6 +112,7 @@ public class ValueResolver implements DeferredSupplier, Iterable implements DeferredSupplier, Iterable swallowExceptions() { this.swallowExceptions = true; return this; } + + public ValueResolver avoidSideEffects() { + this.avoidSideEffects = true; + return this; + } /** whether the task should be marked as transient; defaults true */ public ValueResolver transientTask(boolean isTransientTask) { @@ -349,7 +356,7 @@ protected Maybe getMaybeInternal() { checkTypeNotNull(); Object v = this.value; - + //if the expected type is a closure or map and that's what we have, we're done (or if it's null); //but not allowed to return a future or DeferredSupplier as the resolved value if (v==null || (!forceDeep && type.isInstance(v) && !Future.class.isInstance(v) && !DeferredSupplier.class.isInstance(v))) @@ -378,6 +385,9 @@ protected Maybe getMaybeInternal() { if (!((TaskAdaptable) v).asTask().isSubmitted() ) { if (exec==null) return Maybe.absent("Value for unsubmitted task '"+getDescription()+"' requested but no execution context available"); + if (v instanceof HasSideEffects && avoidSideEffects) { + return Maybe.absent(); + } exec.submit(((TaskAdaptable) v).asTask()); } } @@ -405,7 +415,9 @@ public Maybe call() throws Exception { } else if (v instanceof DeferredSupplier) { final DeferredSupplier ds = (DeferredSupplier) v; - + if (v instanceof HasSideEffects && avoidSideEffects) { + return Maybe.absent(); + } if ((!Boolean.FALSE.equals(embedResolutionInTask) && (exec!=null || timeout!=null)) || Boolean.TRUE.equals(embedResolutionInTask)) { if (exec==null) return Maybe.absent("Embedding in task needed for '"+getDescription()+"' but no execution context available"); diff --git a/core/src/test/java/org/apache/brooklyn/core/test/entity/TestEntity.java b/core/src/test/java/org/apache/brooklyn/core/test/entity/TestEntity.java index 7a29c1a93d..6233675b27 100644 --- a/core/src/test/java/org/apache/brooklyn/core/test/entity/TestEntity.java +++ b/core/src/test/java/org/apache/brooklyn/core/test/entity/TestEntity.java @@ -100,6 +100,9 @@ public interface TestEntity extends Entity, Startable, EntityLocal, EntityIntern @Effector(description="returns the arg passed in") public Object identityEffector(@EffectorParam(name="arg", description="val to return") Object arg); + + @Effector(description="an example effector with side effects, returns a strictly increasing sequence value") + public Integer sequenceEffector(); @Effector(description="sleeps for the given duration") public void sleepEffector(@EffectorParam(name="duration") Duration duration); diff --git a/core/src/test/java/org/apache/brooklyn/core/test/entity/TestEntityImpl.java b/core/src/test/java/org/apache/brooklyn/core/test/entity/TestEntityImpl.java index 7a2c813278..59fcac3325 100644 --- a/core/src/test/java/org/apache/brooklyn/core/test/entity/TestEntityImpl.java +++ b/core/src/test/java/org/apache/brooklyn/core/test/entity/TestEntityImpl.java @@ -56,6 +56,7 @@ public class TestEntityImpl extends AbstractEntity implements TestEntity { protected int sequenceValue = 0; protected AtomicInteger counter = new AtomicInteger(0); + protected AtomicInteger effectorCount = new AtomicInteger(0); protected Map constructorProperties; protected Map configureProperties; protected List callHistory = Collections.synchronizedList(Lists.newArrayList()); @@ -97,7 +98,13 @@ public Object identityEffector(Object arg) { callHistory.add("identityEffector"); return checkNotNull(arg, "arg"); } - + + @Override + public Integer sequenceEffector() { + callHistory.add("sequenceEffector"); + return effectorCount.incrementAndGet(); + } + @Override public void sleepEffector(Duration duration) { if (LOG.isTraceEnabled()) LOG.trace("In sleepEffector for {}", this);