Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.concurrent.Executor;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.util.guava.Maybe;

/**
* This is a Brooklyn extension to the Java {@link Executor}.
Expand Down Expand Up @@ -64,4 +65,6 @@ public interface ExecutionContext extends Executor {

boolean isShutdown();

<T> Maybe<T> getImmediately(Object callableOrSupplier);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding javadoc here - e.g. similar to what you've added in the impl BasicExecutionContext. Worth saying when it will return Maybe.absent (e.g. if the task execution requires blocking for other work, and can't complete in a timely fashion).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea. we should maybe move ImmediateSupplier to the utils package, then we could reference its javadoc? we might also change the return type to be ReferenceWithError<Maybe<T>> so the "can't immediately tell if there's a value" problem state can be detected without throwing. have marked @Beta for now.


}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.apache.brooklyn.util.core.task.DeferredSupplier;
import org.apache.brooklyn.util.core.task.ImmediateSupplier;
import org.apache.brooklyn.util.core.task.TaskBuilder;
import org.apache.brooklyn.util.core.task.TaskTags;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.groovy.GroovyJavaMethods;
Expand Down Expand Up @@ -206,6 +207,15 @@ public Maybe<Entity> getImmediately() {
}
}

@Override
public Entity get() {
try {
return call();
} catch (Exception e) {
throw Exceptions.propagate(e);
}
}

@Override
public Entity call() throws Exception {
return callImpl(false).get();
Expand All @@ -219,7 +229,7 @@ protected Maybe<Entity> getEntity(boolean immediate) {
return Maybe.of(scopeComponent.get());
}
} else {
return Maybe.<Entity>of(entity());
return Maybe.<Entity>ofDisallowingNull(entity()).or(Maybe.<Entity>absent("Context entity not available when trying to evaluate Brooklyn DSL"));
}
}

Expand Down Expand Up @@ -311,10 +321,11 @@ protected Maybe<Entity> callImpl(boolean immediate) throws Exception {
return Maybe.of(result.get());
}

// TODO may want to block and repeat on new entities joining?
throw new NoSuchElementException("No entity matching id " + desiredComponentId+
// could be nice if DSL has an extra .block() method to allow it to wait for a matching entity.
// previously we threw if nothing existed; now we return an absent with a detailed error
return Maybe.absent(new NoSuchElementException("No entity matching id " + desiredComponentId+
(scope==Scope.GLOBAL ? "" : ", in scope "+scope+" wrt "+entity+
(scopeComponent!=null ? " ("+scopeComponent+" from "+entity()+")" : "")));
(scopeComponent!=null ? " ("+scopeComponent+" from "+entity()+")" : ""))));
}

private ExecutionContext getExecutionContext() {
Expand Down Expand Up @@ -539,8 +550,9 @@ protected String resolveKeyName(boolean immediately) {
@Override
public final Maybe<Object> getImmediately() {
Maybe<Entity> targetEntityMaybe = component.getImmediately();
if (targetEntityMaybe.isAbsent()) return Maybe.absent("Target entity not available");
if (targetEntityMaybe.isAbsent()) return Maybe.<Object>cast(targetEntityMaybe);
EntityInternal targetEntity = (EntityInternal) targetEntityMaybe.get();
checkAndTagForRecursiveReference(targetEntity);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading this right... it adds the tag to the current task, but then the tag is not removed at the end of this method - should it be?

@ahgittin ahgittin Feb 18, 2017

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the tag is left on the task. you're right that could cause problems if the calling code isn't in a task (maybe it always will be but safer not to assume).

UPDATE: we now always use a dedicated tag

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually i've done a better strategy -- @aledsage appreciate any thoughts on this:

                // don't check on ourself; only look higher in hierarchy;
                // this assumes impls always spawn new tasks (which they do, just maybe not always in new threads)
                // but it means it does not rely on tag removal to prevent weird errors, 
                // and more importantly it makes the strategy idempotent

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but actually this won't work either will it --

consider P1 needs to evaluate key C1, then key C2, and C2 refers to C1. if it isn't a dedicated subtask the second check will fail.

guess we need to ensure a dedicated subtask. :( . i'll see whether we can do that, probably with an "official" task tag type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did it, but without an official task type -- easy enough to get the routines to share code and trigger a dedicated (non-thread) task


String keyNameS = resolveKeyName(true);
ConfigKey<?> key = targetEntity.getEntityType().getConfigKey(keyNameS);
Expand All @@ -558,11 +570,26 @@ public Task<Object> newTask() {
@Override
public Object call() throws Exception {
Entity targetEntity = component.get();
checkAndTagForRecursiveReference(targetEntity);

String keyNameS = resolveKeyName(true);
ConfigKey<?> key = targetEntity.getEntityType().getConfigKey(keyNameS);
return targetEntity.getConfig(key != null ? key : ConfigKeys.newConfigKey(Object.class, keyNameS));
}})
.build();
}
}).build();
}

private void checkAndTagForRecursiveReference(Entity targetEntity) {
String tag = "DSL:entity('"+targetEntity.getId()+"').config('"+keyName+"')";
Task<?> ancestor = Tasks.current();
while (ancestor!=null) {
if (TaskTags.hasTag(ancestor, tag)) {
throw new IllegalStateException("Recursive config reference "+tag);
}
ancestor = ancestor.getSubmittedByTask();
}

Tasks.addTagDynamically(tag);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.exceptions.RuntimeInterruptedException;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterMethod;
Expand All @@ -44,7 +50,6 @@

public class ConfigYamlTest extends AbstractYamlTest {

@SuppressWarnings("unused")
private static final Logger LOG = LoggerFactory.getLogger(ConfigYamlTest.class);

private ExecutorService executor;
Expand Down Expand Up @@ -91,6 +96,62 @@ public void testConfigInConfigBlock() throws Exception {
assertNull(entity.getMyField()); // field with @SetFromFlag
assertNull(entity.getMyField2()); // field with @SetFromFlag("myField2Alias"), set using alias
}


@Test
public void testRecursiveConfigFailsGracefully() throws Exception {
doTestRecursiveConfigFailsGracefully(false);
}

// TODO this test fails because entities aren't available when evaluating immediately

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes for me - when/why does it fail, or does this comment need deleted?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stale comment, good catch

@Test
public void testRecursiveConfigImmediateFailsGracefully() throws Exception {
doTestRecursiveConfigFailsGracefully(true);
}

protected void doTestRecursiveConfigFailsGracefully(boolean immediate) throws Exception {
String yaml = Joiner.on("\n").join(
"services:",
"- type: org.apache.brooklyn.core.test.entity.TestEntity",
" brooklyn.config:",
" infinite_loop: $brooklyn:config(\"infinite_loop\")");

final Entity app = createStartWaitAndLogApplication(yaml);
TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren());

Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Time.sleep(Duration.FIVE_SECONDS);
// error, loop wasn't interrupted or detected
LOG.warn("Timeout elapsed, destroying items; usage: "+
((LocalManagementContext)mgmt()).getGarbageCollector().getUsageString());
//Entities.destroy(app);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete commented out code, or add additional comment to say when one would uncomment it.

} catch (RuntimeInterruptedException e) {
// expected on normal execution
Thread.interrupted();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you calling this to clear the interrupted status? Why? Do you get an ugly exception or something if we don't?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exactly, comment added

}
}
});
t.start();
try {
String c;
if (immediate) {
// this should throw rather than return "absent", because the error is definitive (absent means couldn't resolve in time)
c = entity.config().getNonBlocking(ConfigKeys.newStringConfigKey("infinite_loop")).or("FAILED");
} else {
c = entity.config().get(ConfigKeys.newStringConfigKey("infinite_loop"));
}
Asserts.shouldHaveFailedPreviously("Expected recursive error, instead got: "+c);
} catch (Exception e) {
Asserts.expectedFailureContainsIgnoreCase(e, "infinite_loop", "recursive");
} finally {
if (!Entities.isManaged(app)) {
t.interrupt();
}
}
}

@Test
public void testConfigAtTopLevel() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.time.Duration;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -296,14 +297,13 @@ public void testUrlEncode() throws Exception {
@Test
public void testEntityNotFound() throws Exception {
BrooklynDslDeferredSupplier<?> dsl = BrooklynDslCommon.entity("myIdDoesNotExist");
Maybe<?> actualValue = execDslImmediately(dsl, Entity.class, app, true);
Assert.assertTrue(actualValue.isAbsent());
try {
Maybe<?> actualValue = execDslImmediately(dsl, Entity.class, app, true);
actualValue.get();
Asserts.shouldHaveFailedPreviously("actual="+actualValue);
} catch (Exception e) {
NoSuchElementException nsee = Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class);
if (nsee == null) {
throw e;
}
Asserts.expectedFailureOfType(e, NoSuchElementException.class);
}
}

Expand Down Expand Up @@ -365,7 +365,7 @@ public DslTestWorker satisfiedAsynchronously(boolean val) {
return this;
}

@SuppressWarnings("unused") // included for completeness?
@SuppressWarnings("unused") // kept in case useful for additional tests, for completeness
public DslTestWorker wrapInTaskForImmediately(boolean val) {
wrapInTaskForImmediately = val;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import javax.annotation.Nullable;

import org.apache.brooklyn.api.mgmt.ExecutionContext;
import org.apache.brooklyn.api.mgmt.TaskFactory;
import org.apache.brooklyn.api.objs.BrooklynObject;
import org.apache.brooklyn.config.ConfigInheritance;
import org.apache.brooklyn.config.ConfigInheritances;
Expand Down Expand Up @@ -231,7 +232,7 @@ public Maybe<Object> getConfigRaw(ConfigKey<?> key, boolean includeInherited) {
}

protected Object coerceConfigVal(ConfigKey<?> key, Object v) {
if ((v instanceof Future) || (v instanceof DeferredSupplier)) {
if ((v instanceof Future) || (v instanceof DeferredSupplier) || (v instanceof TaskFactory)) {
// no coercion for these (coerce on exit)
return v;
} else if (key instanceof StructuredConfigKey) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ public class BrooklynTaskTags extends TaskTags {
* and that it need not appear in some task lists;
* often used for framework lifecycle events and sensor polling */
public static final String TRANSIENT_TASK_TAG = "TRANSIENT";
/** marks that a task is meant to return immediately, without blocking (or if absolutely necessary blocking for a short while) */
public static final String IMMEDIATE_TASK_TAG = "IMMEDIATE";

// ------------- entity tags -------------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ protected <T> Maybe<T> getNonBlockingResolvingSimple(ConfigKey<T> key) {
.immediately(true)
.deep(true)
.context(getContext())
.swallowExceptions()
.get();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably agree with this change, but don't feel confident about the full implications of it throwing the exception rather than returning the default value versus absent. In your test doTestRecursiveConfigFailsGracefully it certainly makes sense, but not sure what else this will affect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, the immediate stuff is quite new so i'd prefer to try this. if it's a problem we should perhaps look at wrapping in a ReferenceWithError as above, then caller will be forced to deal with the failure in the appropriate way.

return (resolved != marker)
? TypeCoercions.tryCoerce(resolved, key.getTypeToken())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags.WrappedEntity;
import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.guava.Maybe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -96,7 +97,33 @@ public ExecutionManager getExecutionManager() {
/** returns tasks started by this context (or tasks which have all the tags on this object) */
@Override
public Set<Task<?>> getTasks() { return executionManager.getTasksWithAllTags(tags); }


/** performs execution without spawning a new task thread, though it does temporarily set a fake task for the purpose of getting context;
* currently supports suppliers or callables */
@SuppressWarnings("unchecked")
@Override
public <T> Maybe<T> getImmediately(Object callableOrSupplier) {
BasicTask<?> fakeTaskForContext = new BasicTask<Object>(MutableMap.of("displayName", "immediate evaluation"));
fakeTaskForContext.tags.addAll(tags);
fakeTaskForContext.tags.add(BrooklynTaskTags.IMMEDIATE_TASK_TAG);
fakeTaskForContext.tags.add(BrooklynTaskTags.TRANSIENT_TASK_TAG);

Task<?> previousTask = BasicExecutionManager.getPerThreadCurrentTask().get();
if (previousTask!=null) fakeTaskForContext.setSubmittedByTask(previousTask);
fakeTaskForContext.cancel();
try {
BasicExecutionManager.getPerThreadCurrentTask().set(fakeTaskForContext);

if (!(callableOrSupplier instanceof ImmediateSupplier)) {
callableOrSupplier = InterruptingImmediateSupplier.of(callableOrSupplier);
}
return ((ImmediateSupplier<T>)callableOrSupplier).getImmediately();

} finally {
BasicExecutionManager.getPerThreadCurrentTask().set(previousTask);
}
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
protected <T> Task<T> submitInternal(Map<?,?> propertiesQ, final Object task) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ public boolean cancel(TaskCancellationMode mode) {
if (!task.isCancelled()) result |= ((TaskInternal<T>)task).cancel(mode);
result |= delegate().cancel(mode.isAllowedToInterruptTask());

if (mode.isAllowedToInterruptAllSubmittedTasks() || mode.isAllowedToInterruptDependentSubmittedTasks()) {
if (mode.isAllowedToInterruptDependentSubmittedTasks()) {
int subtasksFound=0;
int subtasksReallyCancelled=0;

Expand Down Expand Up @@ -753,7 +753,10 @@ protected void beforeStartAtomicTask(Map<?,?> flags, Task<?> task) {
/** invoked in a task's thread when a task is starting to run (may be some time after submitted),
* but before doing any of the task's work, so that we can update bookkeeping and notify callbacks */
protected void internalBeforeStart(Map<?,?> flags, Task<?> task) {
activeTaskCount.incrementAndGet();
int count = activeTaskCount.incrementAndGet();
if (count % 1000==0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we hover around the 999 to 1001 mark for the number of active tasks, then we'll get this log message lots of times. But I think that's acceptable, in exchange for simpler code. So fine as it is.

log.warn("High number of active tasks: task #"+count+" is "+task);
}

//set thread _before_ start time, so we won't get a null thread when there is a start-time
if (log.isTraceEnabled()) log.trace(""+this+" beforeStart, task: "+task + " running on thread " + Thread.currentThread().getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,11 @@ public void queue(Task<?> t) {
@Override
protected boolean doCancel(TaskCancellationMode mode) {
boolean result = false;
if (mode.isAllowedToInterruptDependentSubmittedTasks() || mode.isAllowedToInterruptAllSubmittedTasks()) {
for (Task<?> t: secondaryJobsAll)
if (mode.isAllowedToInterruptDependentSubmittedTasks()) {
for (Task<?> t: secondaryJobsAll) {
// secondary jobs are dependent
result = ((TaskInternal<?>)t).cancel(mode) || result;
}
}
return super.doCancel(mode) || result;
// returns true if anything is successfully cancelled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@

import org.apache.brooklyn.util.guava.Maybe;

import com.google.common.base.Supplier;

/**
* A class that supplies objects of a single type, without blocking for any significant length
* of time.
* A {@link Supplier} that has an extra method capable of supplying a value immediately or an absent if definitely not available,
* or throwing an {@link ImmediateUnsupportedException} if it cannot determine whether a value is immediately available.
*/
public interface ImmediateSupplier<T> {
public interface ImmediateSupplier<T> extends Supplier<T> {

/**
* Indicates that we are unable to get the value immediately, because that is not supported
* Indicates that a supplier does not support immediate evaluation,
* i.e. it may need to block to evaluate even if there is a value available
* (e.g. because the supplier is composed of sub-tasks that do not support {@link ImmediateSupplier}.
*/
public static class ImmediateUnsupportedException extends UnsupportedOperationException {
Expand All @@ -44,7 +47,7 @@ public ImmediateUnsupportedException(String message, Throwable cause) {
/**
* Gets the value promptly, or returns {@link Maybe#absent()} if the value is not yet available.
*
* @throws ImmediateUnsupportedException if cannot determinte the value immediately
* @throws ImmediateUnsupportedException if cannot determine whether a value is immediately available
*/
Maybe<T> getImmediately();
}
Loading