Java Code Examples for org.apache.brooklyn.core.entity.Entities#invokeEffector()

The following examples show how to use org.apache.brooklyn.core.entity.Entities#invokeEffector() . You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example 1
Source File: SoftwareEffectorTest.java    From brooklyn-server with Apache License 2.0 6 votes vote down vote up
@Test(groups="Integration")
public void testBadExitCodeCaught() {
    Task<Void> call = Entities.invokeEffector(app, app, Effectors.effector(Void.class, "badExitCode")
            .impl(new SshEffectorBody<Void>() {
                @Override
                public Void call(ConfigBag parameters) {
                    queue( ssh(COMMAND_THAT_DOES_NOT_EXIST).requiringZeroAndReturningStdout() );
                    return null;
                }
            }).build() );
    try {
        Object result = call.getUnchecked();
        Assert.fail("ERROR: should have failed earlier in this test, instead got successful task result "+result+" from "+call);
    } catch (Exception e) {
        Throwable root = Throwables.getRootCause(e);
        if (!(root instanceof IllegalStateException)) Assert.fail("Should have failed with IAE, but got: "+root);
        if (root.getMessage()==null || root.getMessage().indexOf("exit code")<=0) 
            Assert.fail("Should have failed with 'exit code' message, but got: "+root);
        // test passed
        return;
    }
}
 
Example 2
Source File: SoftwareEffectorTest.java    From brooklyn-server with Apache License 2.0 6 votes vote down vote up
@Test(groups="Integration")
public void testBadExitCodeCaughtAndStdErrAvailable() {
    final ProcessTaskWrapper<?>[] sshTasks = new ProcessTaskWrapper[1];
    
    Task<Void> call = Entities.invokeEffector(app, app, Effectors.effector(Void.class, "badExitCode")
            .impl(new SshEffectorBody<Void>() {
                @Override
                public Void call(ConfigBag parameters) {
                    sshTasks[0] = queue( ssh(COMMAND_THAT_DOES_NOT_EXIST).requiringExitCodeZero() );
                    return null;
                }
            }).build() );
    call.blockUntilEnded();
    Assert.assertTrue(call.isError());
    log.info("stderr gives: "+new String(sshTasks[0].getStderr()));
    Assert.assertTrue(new String(sshTasks[0].getStderr()).indexOf(COMMAND_THAT_DOES_NOT_EXIST) >= 0);
}
 
Example 3
Source File: SoftwareProcessEntityLatchTest.java    From brooklyn-server with Apache License 2.0 6 votes vote down vote up
public void doTestLatchBlocks(ConfigKey<Boolean> latch, List<String> preLatchEvents, Object latchValue, Function<? super MyService, Void> customAssertFn) throws Exception {
    final AttributeSensor<Object> latchSensor = Sensors.newSensor(Object.class, "latch");
    final MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class)
            .configure(ConfigKeys.newConfigKey(Object.class, latch.getName()), (Object)DependentConfiguration.attributeWhenReady(app, latchSensor)));

    final Task<Void> task;
    final Task<Void> startTask = Entities.invokeEffector(app, app, MyService.START, ImmutableMap.of("locations", ImmutableList.of(loc)));
    if (latch != SoftwareProcess.STOP_LATCH) {
        task = startTask;
    } else {
        startTask.get(Duration.THIRTY_SECONDS);
        task = Entities.invokeEffector(app, app, MyService.STOP);
    }

    assertEffectorBlockingDetailsEventually(entity, task.getDisplayName(), "Waiting for config " + latch.getName());
    assertDriverEventsEquals(entity, preLatchEvents);
    assertFalse(task.isDone());

    app.sensors().set(latchSensor, latchValue);

    customAssertFn.apply(entity);

    task.get(Duration.THIRTY_SECONDS);
    assertDriverEventsEquals(entity, getLatchPostTasks(latch));
}
 
Example 4
Source File: ServiceRestarter.java    From brooklyn-server with Apache License 2.0 6 votes vote down vote up
protected synchronized void onDetectedFailure(SensorEvent<Object> event) {
    if (isSuspended()) {
        highlightViolation("Failure detected but policy suspended");
        LOG.warn("ServiceRestarter suspended, so not acting on failure detected at "+entity+" ("+event.getValue()+")");
        return;
    }

    LOG.warn("ServiceRestarter acting on failure detected at "+entity+" ("+event.getValue()+")");
    long current = System.currentTimeMillis();
    Long last = lastFailureTime.getAndSet(current);
    long elapsed = last==null ? -1 : current-last;
    if (elapsed>=0 && elapsed <= getConfig(FAIL_ON_RECURRING_FAILURES_IN_THIS_DURATION).toMilliseconds()) {
        highlightViolation("Failure detected but policy ran "+Duration.millis(elapsed)+" ago (cannot run again within "+getConfig(FAIL_ON_RECURRING_FAILURES_IN_THIS_DURATION)+")");
        onRestartFailed("Restart failure (failed again after "+Time.makeTimeStringRounded(elapsed)+") at "+entity+": "+event.getValue());
        return;
    }
    try {
        highlightViolation("Failure detected and restart triggered");
        ServiceStateLogic.setExpectedState(entity, Lifecycle.STARTING);
        Task<Void> t = Entities.invokeEffector(entity, entity, Startable.RESTART);
        highlightAction("Restart node on failure", t);
        t.get();
    } catch (Exception e) {
        onRestartFailed("Restart failure (error "+e+") at "+entity+": "+event.getValue());
    }
}
 
Example 5
Source File: MachineLifecycleEffectorTasksTest.java    From brooklyn-server with Apache License 2.0 5 votes vote down vote up
@Test(groups="Integration")
public void testProvisionLatchObeyed() throws Exception {

    AttributeSensor<Boolean> ready = Sensors.newBooleanSensor("readiness");

    BasicEntity triggerEntity = app.createAndManageChild(EntitySpec.create(BasicEntity.class));

    EmptySoftwareProcess entity = app.createAndManageChild(EntitySpec.create(EmptySoftwareProcess.class)
            .configure(BrooklynConfigKeys.PROVISION_LATCH, DependentConfiguration.attributeWhenReady(triggerEntity, ready)));

    final Task<Void> task = Entities.invokeEffector(app, app, Startable.START, ImmutableMap.of(
            "locations", ImmutableList.of(BailOutJcloudsLocation.newBailOutJcloudsLocation(app.getManagementContext()))));
    
    Time.sleep(ValueResolver.PRETTY_QUICK_WAIT);
    if (task.isDone()) throw new IllegalStateException("Task finished early with: "+task.get());
    assertEffectorBlockingDetailsEventually(entity, "Waiting for config " + BrooklynConfigKeys.PROVISION_LATCH.getName());

    Asserts.succeedsContinually(new Runnable() {
        @Override
        public void run() {
            if (task.isDone()) throw new IllegalStateException("Task finished early with: "+task.getUnchecked());
        }
    });
    try {
        triggerEntity.sensors().set(ready, true);
        task.get(Duration.THIRTY_SECONDS);
    } catch (Throwable t) {
        Exceptions.propagateIfFatal(t);
        if ((t.toString().contains(BailOutJcloudsLocation.ERROR_MESSAGE))) {
            // expected - BailOut location throws - just swallow
        } else {
            Exceptions.propagate(t);
        }
    }
}
 
Example 6
Source File: SoftwareProcessEntityLatchTest.java    From brooklyn-server with Apache License 2.0 5 votes vote down vote up
/**
 * Deploy a cluster of 4 SoftwareProcesses, which all share the same CountingLatch instance so
 * that only two can obtain it at a time. The {@link CountingLatch} is uses the default
 * of sleeping for 100ms after acquiring the lock, so it is very likely in a non-contended 
 * machine that we'll hit max concurrency. We assert that the maximum number of things holding
 * the lock at any one time was exactly 2.
 * 
 * This is marked as an "integration" test because it is time-sensitive. If executed on a
 * low-performance machine (e.g. Apache Jenkins, where other docker containers are contending
 * for resources) then it might not get to 2 things holding the lock at the same time.
 */
@Test(groups="Integration", dataProvider="latchAndTaskNamesProvider", timeOut=Asserts.THIRTY_SECONDS_TIMEOUT_MS)
public void testConcurrency(ConfigKey<Boolean> latch, List<String> _ignored) throws Exception {
    LocalhostMachineProvisioningLocation loc = app.newLocalhostProvisioningLocation(ImmutableMap.of("address", "127.0.0.1"));
    
    final int maxConcurrency = 2;
    final ReleaseableLatch latchSemaphore = ReleaseableLatch.Factory.newMaxConcurrencyLatch(maxConcurrency);
    final AttributeSensor<Object> latchSensor = Sensors.newSensor(Object.class, "latch");
    final CountingLatch countingLatch = new CountingLatch(latchSemaphore, maxConcurrency);
    @SuppressWarnings({"unused"})
    DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
            .configure(DynamicCluster.INITIAL_SIZE, maxConcurrency*2)
            .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(MyService.class)
                    .configure(ConfigKeys.newConfigKey(Object.class, latch.getName()), (Object)DependentConfiguration.attributeWhenReady(app, latchSensor))));
    app.sensors().set(latchSensor, countingLatch);
    final Task<Void> startTask = Entities.invokeEffector(app, app, MyService.START, ImmutableMap.of("locations", ImmutableList.of(loc)));
    startTask.get();
    final Task<Void> stopTask = Entities.invokeEffector(app, app, MyService.STOP, ImmutableMap.<String, Object>of());
    stopTask.get();
    assertEquals(countingLatch.getCounter(), 0);
    // Check we have actually used the latch
    assertNotEquals(countingLatch.getMaxCounter(), 0, "Latch not acquired at all");
    // In theory this is 0 < maxCnt <= maxConcurrency contract, but in practice
    // we should always reach the maximum due to the sleeps in CountingLatch.
    // Change if found to fail in the wild.
    assertEquals(countingLatch.getMaxCounter(), maxConcurrency);
}
 
Example 7
Source File: SoftwareProcessEntityLatchTest.java    From brooklyn-server with Apache License 2.0 5 votes vote down vote up
/**
 * Deploy a cluster of 4 SoftwareProcesses, which all share the same CountingLatch instance so
 * that only two can obtain it at a time. The {@link CountingLatch} is configured with a really
 * long sleep after it acquires the lock, so we should have just 2 entities having acquired it
 * and the others blocked.
 * 
 * We assert that we got into this state, and then we tear it down by unmanaging the cluster 
 * (we unmanage because the cluster would otherwise takes ages due to the sleep in 
 * {@link CountingLatch}.
 */
@Test(dataProvider="latchAndTaskNamesProvider", timeOut=Asserts.THIRTY_SECONDS_TIMEOUT_MS)
public void testConcurrencyAllowsExactlyMax(ConfigKey<Boolean> latch, List<String> _ignored) throws Exception {
    boolean isLatchOnStop = latch.getName().contains("stop");
    LocalhostMachineProvisioningLocation loc = app.newLocalhostProvisioningLocation(ImmutableMap.of("address", "127.0.0.1"));
    
    final int maxConcurrency = 2;
    final ReleaseableLatch latchSemaphore = ReleaseableLatch.Factory.newMaxConcurrencyLatch(maxConcurrency);
    final AttributeSensor<Object> latchSensor = Sensors.newSensor(Object.class, "latch");
    final CountingLatch countingLatch = new CountingLatch(latchSemaphore, maxConcurrency, Asserts.DEFAULT_LONG_TIMEOUT.multiply(2));
    
    DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
            .configure(DynamicCluster.INITIAL_SIZE, maxConcurrency*2)
            .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(MyService.class)
                    .configure(ConfigKeys.newConfigKey(Object.class, latch.getName()), (Object)DependentConfiguration.attributeWhenReady(app, latchSensor))));
    app.sensors().set(latchSensor, countingLatch);
    
    try {
        if (isLatchOnStop) {
            // Start will complete; then invoke stop async (don't expect it to complete!)
            app.start(ImmutableList.of(loc));
            Entities.invokeEffector(app, app, MyService.STOP, ImmutableMap.<String, Object>of());
        } else {
            // Invoke start async (don't expect it to complete!)
            Entities.invokeEffector(app, app, MyService.START, ImmutableMap.of("locations", ImmutableList.of(loc)));
        }
        
        // Because CountingLatch waits for ages, we'll eventually have maxConcurrent having successfully 
        // acquired, but the others blocked. Wait for that, and assert it stays that way.
        countingLatch.assertMaxCounterEventually(Predicates.equalTo(maxConcurrency));
        countingLatch.assertMaxCounterContinually(Predicates.equalTo(maxConcurrency), Duration.millis(100));
    } finally {
        // Don't wait for cluster to start/stop (because of big sleep in CountingLatch) - unmanage it.
        Entities.unmanage(cluster);
    }
}
 
Example 8
Source File: SoftwareProcessEntityLatchTest.java    From brooklyn-server with Apache License 2.0 5 votes vote down vote up
@Test(dataProvider="latchAndTaskNamesProvider"/*, timeOut=Asserts.THIRTY_SECONDS_TIMEOUT_MS*/)
public void testFailedReleaseableUnblocks(final ConfigKey<Boolean> latch, List<String> _ignored) throws Exception {
    LocalhostMachineProvisioningLocation loc = app.newLocalhostProvisioningLocation(ImmutableMap.of("address", "127.0.0.1"));

    final int maxConcurrency = 1;
    final ReleaseableLatch latchSemaphore = ReleaseableLatch.Factory.newMaxConcurrencyLatch(maxConcurrency);
    final AttributeSensor<Object> latchSensor = Sensors.newSensor(Object.class, "latch");
    final CountingLatch countingLatch = new CountingLatch(latchSemaphore, maxConcurrency);
    // FIRST_MEMBER_SPEC latches are not guaranteed to be acquired before MEMBER_SPEC latches
    // so the start effector could complete, but the counting latch will catch if there are
    // any unreleased semaphores.
    @SuppressWarnings({"unused"})
    DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
            .configure(DynamicCluster.INITIAL_SIZE, 2)
            .configure(DynamicCluster.FIRST_MEMBER_SPEC, EntitySpec.create(FailingMyService.class)
                    .configure(ConfigKeys.newConfigKey(Object.class, latch.getName()), (Object)DependentConfiguration.attributeWhenReady(app, latchSensor)))
            .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(MyService.class)
                    .configure(ConfigKeys.newConfigKey(Object.class, latch.getName()), (Object)DependentConfiguration.attributeWhenReady(app, latchSensor))));
    app.sensors().set(latchSensor, countingLatch);
    final Task<Void> startTask = Entities.invokeEffector(app, app, MyService.START, ImmutableMap.of("locations", ImmutableList.of(loc)));
    //expected to fail but should complete quickly
    assertTrue(startTask.blockUntilEnded(Asserts.DEFAULT_LONG_TIMEOUT), "timeout waiting for start effector to complete");
    assertTrue(latch == SoftwareProcess.STOP_LATCH || startTask.isError());
    final Task<Void> stopTask = Entities.invokeEffector(app, app, MyService.STOP, ImmutableMap.<String, Object>of());
    //expected to fail but should complete quickly
    assertTrue(stopTask.blockUntilEnded(Asserts.DEFAULT_LONG_TIMEOUT), "timeout waiting for stop effector to complete");
    // stop task won't fail because the process stop failed; the error is ignored
    assertTrue(stopTask.isDone());
    assertEquals(countingLatch.getCounter(), 0);
    // Check we have actually used the latch
    assertNotEquals(countingLatch.getMaxCounter(), 0, "Latch not acquired at all");
    // In theory this is 0 < maxCnt <= maxConcurrency contract, but in practice
    // we should always reach the maximum due to the sleeps in CountingLatch.
    // Change if found to fail in the wild.
    assertEquals(countingLatch.getMaxCounter(), maxConcurrency);
}
 
Example 9
Source File: DynamicFabricImpl.java    From brooklyn-server with Apache License 2.0 5 votes vote down vote up
@Override
public void stop() {
    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();
        ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPED);
        sensors().set(SERVICE_UP, false);
    } catch (Exception e) {
        ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
        throw Exceptions.propagate(e);
    }
}
 
Example 10
Source File: EntityManagementUtils.java    From brooklyn-server with Apache License 2.0 5 votes vote down vote up
public static <T extends Application> CreationResult<T,Void> start(T app) {
    Task<Void> task = Entities.invokeEffector(app, app, Startable.START,
        // locations already set in the entities themselves;
        // TODO make it so that this arg does not have to be supplied to START !
        MutableMap.of("locations", MutableList.of()));
    return CreationResult.of(app, task);
}
 
Example 11
Source File: SoftwareEffectorTest.java    From brooklyn-server with Apache License 2.0 4 votes vote down vote up
@Test(groups="Integration")
public void testSshDateEffector1() {
    Task<String> call = Entities.invokeEffector(app, app, GET_REMOTE_DATE_1);
    log.info("ssh date 1 gives: "+call.getUnchecked());
    Assert.assertTrue(call.getUnchecked().indexOf("201") > 0);
}
 
Example 12
Source File: SoftwareEffectorTest.java    From brooklyn-server with Apache License 2.0 4 votes vote down vote up
@Test(groups="Integration")
public void testSshDateEffector2() {
    Task<String> call = Entities.invokeEffector(app, app, GET_REMOTE_DATE_2);
    log.info("ssh date 2 gives: "+call.getUnchecked());
    Assert.assertTrue(call.getUnchecked().indexOf("201") == 0);
}
 
Example 13
Source File: SoftwareProcessStopsDuringStartJcloudsLiveTest.java    From brooklyn-server with Apache License 2.0 4 votes vote down vote up
/**
 * Verifies the behavior described in
 * <a href="https://issues.apache.org/jira/browse/BROOKLYN-264">BROOKLYN-264 Stop app while VM still being provisioned: vm is left running when app is expunged</a>
 * <ul>
 *     <li>Launch the app
 *     <li>wait a few seconds (for entity internal state to indicate the provisioning is happening)
 *     <li>Expunge the app (thus calling stop on the entity)
 *     <li>assert the image is terminated (using jclouds directly to query the cloud api)
 * </ul>
 */
@Test(groups = {"Live"})
public void testJclousMachineIsExpungedWhenStoppedDuringStart() throws Exception {
    Map<String,?> allFlags = ImmutableMap.<String,Object>builder()
            .put("tags", ImmutableList.of(getClass().getName()))
            .put(JcloudsLocation.IMAGE_ID.getName(), IMAGE_ID)
            .put(JcloudsLocation.HARDWARE_ID.getName(), HARDWARE_ID)
            .put(LocationConfigKeys.CLOUD_MACHINE_NAMER_CLASS.getName(), "")
            .put(JcloudsLocation.MACHINE_CREATE_ATTEMPTS.getName(), 1)
            .put(JcloudsLocation.OPEN_IPTABLES.getName(), true)
            .build();
    JcloudsLocation jcloudsLocation = (JcloudsLocation)mgmt.getLocationRegistry().getLocationManaged(LOCATION_SPEC, allFlags);

    final VanillaSoftwareProcess entity = app.createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class)
            .configure(VanillaSoftwareProcess.INSTALL_COMMAND, "echo install")
            .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, "echo launch")
            .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "echo running"));

    app.addLocations(ImmutableList.of(jcloudsLocation));

    // Invoke async
    @SuppressWarnings("unused")
    Task<Void> startTask = Entities.invokeEffector(app, app, Startable.START, ImmutableMap.of("locations", MutableList.of()));
    EntityAsserts.assertAttributeEqualsEventually(entity, AttributesInternal.INTERNAL_PROVISIONING_TASK_STATE, AttributesInternal.ProvisioningTaskState.RUNNING);

    Stopwatch stopwatch = Stopwatch.createStarted();
    Entities.destroyCatching(app);
    LOG.info("Time for expunging: {}", Duration.of(stopwatch));

    NodeMetadata nodeMetadata = Iterables.getFirst(((AWSEC2ComputeService) jcloudsLocation.getComputeService()).listNodesDetailsMatching(new Predicate<ComputeMetadata>() {
            @Override public boolean apply(@Nullable ComputeMetadata computeMetadata) {
                return ((NodeMetadata)computeMetadata).getGroup() == null 
                        ? false
                        : Pattern.matches(
                            "brooklyn-.*" + System.getProperty("user.name") + ".*vanillasoftware.*"+entity.getId().substring(0, 4),
                            ((NodeMetadata)computeMetadata).getGroup()
                            );
            }}),
        null);
    assertNotNull(nodeMetadata, "node matching node found");
    LOG.info("nodeMetadata found after app was created: {}", nodeMetadata);
    
    // If pending (e.g. "status=PENDING[shutting-down]"), wait for it to transition
    Stopwatch pendingStopwatch = Stopwatch.createStarted();
    Duration maxPendingWait = Duration.FIVE_MINUTES;
    while (maxPendingWait.isLongerThan(Duration.of(pendingStopwatch)) && nodeMetadata.getStatus() == NodeMetadata.Status.PENDING) {
        Thread.sleep(1000);
        nodeMetadata = ((AWSEC2ComputeService) jcloudsLocation.getComputeService()).getNodeMetadata(nodeMetadata.getId());
    }
    
    if (nodeMetadata.getStatus() != NodeMetadata.Status.TERMINATED) {
        // Try to terminate the VM - don't want test to leave it behind!
        String errMsg = "The application should be destroyed after stop effector was called: status="+nodeMetadata.getStatus()+"; node="+nodeMetadata;
        LOG.error(errMsg);
        jcloudsLocation.getComputeService().destroyNode(nodeMetadata.getId());
        fail(errMsg);
    }
}
 
Example 14
Source File: BrooklynRestResourceUtils.java    From brooklyn-server with Apache License 2.0 4 votes vote down vote up
public Task<?> start(Application app, List<? extends Location> locations) {
    return Entities.invokeEffector(app, app, Startable.START,
            MutableMap.of("locations", locations));
}
 
Example 15
Source File: TestSensorTest.java    From brooklyn-server with Apache License 2.0 4 votes vote down vote up
@Test
public void testDoesNotLogStacktraceRepeatedly() throws Exception {
    final long time = System.currentTimeMillis();
    final String sensorValue = String.format("%s%s%s", Identifiers.makeRandomId(8), time, Identifiers.makeRandomId(8));
    
    // Test case will repeatedly fail until we have finished our logging assertions.
    // Then we'll let it complete by setting the sensor.
    TestSensor testCase = app.createAndManageChild(EntitySpec.create(TestSensor.class)
            .configure(TestSensor.TIMEOUT, Asserts.DEFAULT_LONG_TIMEOUT)
            .configure(TestSensor.BACKOFF_TO_PERIOD, Duration.millis(1))
            .configure(TestSensor.TARGET_ENTITY, app)
            .configure(TestSensor.SENSOR_NAME, STRING_SENSOR.getName())
            .configure(TestSensor.ASSERTIONS, newListAssertion("matches", String.format(".*%s.*", time))));

    String loggerName = Repeater.class.getName();
    ch.qos.logback.classic.Level logLevel = ch.qos.logback.classic.Level.DEBUG;
    Predicate<ILoggingEvent> repeatedFailureMsgMatcher = EventPredicates.containsMessage("repeated failure; excluding stacktrace");
    Predicate<ILoggingEvent> stacktraceMatcher = EventPredicates.containsExceptionStackLine(TestFrameworkAssertions.class, "checkActualAgainstAssertions");
    Predicate<ILoggingEvent> filter = Predicates.or(repeatedFailureMsgMatcher, stacktraceMatcher);
    try (LogWatcher watcher = new LogWatcher(loggerName, logLevel, filter)) {
        // Invoke async; will let it complete after we see the log messages we expect
        Task<?> task = Entities.invokeEffector(app, app, Startable.START, ImmutableMap.of("locations", locs));

        // Expect "excluding stacktrace" message at least once
        List<ILoggingEvent> repeatedFailureMsgEvents = watcher.assertHasEventEventually(repeatedFailureMsgMatcher);
        assertTrue(repeatedFailureMsgEvents.size() > 0, "repeatedFailureMsgEvents="+repeatedFailureMsgEvents.size());

        // Expect stacktrace just once
        List<ILoggingEvent> stacktraceEvents = watcher.assertHasEventEventually(stacktraceMatcher);
        assertEquals(Integer.valueOf(stacktraceEvents.size()), Integer.valueOf(1), "stacktraceEvents="+stacktraceEvents.size());
        
        //Set STRING sensor
        app.sensors().set(STRING_SENSOR, sensorValue);
        task.get(Asserts.DEFAULT_LONG_TIMEOUT);
        
        assertTestSensorSucceeds(testCase);
        
        // And for good measure (in case we just checked too early last time), check again 
        // that we didn't get another stacktrace
        stacktraceEvents = watcher.getEvents(stacktraceMatcher);
        assertEquals(Integer.valueOf(stacktraceEvents.size()), Integer.valueOf(1), "stacktraceEvents="+stacktraceEvents.size());
    }
}