diff --git a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/ensembles/EnsembleFactory.java b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/ensembles/EnsembleFactory.java index 661b75a2b..87d58e6b3 100644 --- a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/ensembles/EnsembleFactory.java +++ b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/ensembles/EnsembleFactory.java @@ -18,16 +18,17 @@ public interface EnsembleFactory { * Returns a collection of ensemble instances that should exist in the system at the moment, given the knowledge stored in the container parameter. * @param container - a knowledge container containing the current state of the system (i.e. knowledge from the POV of a single component). * @return Currently existing ensemble instances. + * @throws EnsembleFormationException */ - Collection createInstances(KnowledgeContainer container); + Collection createInstances(KnowledgeContainer container) throws EnsembleFormationException; /** - * Gets the initial offset of the ensemble formation. + * Gets the initial scheduling offset of the ensemble formation. * @return Ensemble formation offset. */ - int getOffset(); + int getSchedulingOffset(); /** - * Gets the period after which the ensemble formation is invoked again. + * Gets the period after which the ensemble formation is scheduled again. * @return Ensemble formation period. */ - int getPeriod(); + int getSchedulingPeriod(); } diff --git a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/ensembles/EnsembleFormationException.java b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/ensembles/EnsembleFormationException.java new file mode 100644 index 000000000..c315a7344 --- /dev/null +++ b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/ensembles/EnsembleFormationException.java @@ -0,0 +1,21 @@ +package cz.cuni.mff.d3s.deeco.ensembles; + +import cz.cuni.mff.d3s.deeco.runtime.DEECoException; +/** + * Exception indicating that the ensemble formation has failed significantly. The exact reason is + * implementation specific and should be attached as cause. Used in {@link EnsembleFactory#createInstances}. + * + * @author Filip Krijt + */ +public class EnsembleFormationException extends DEECoException { + + private static final long serialVersionUID = 1L; + + public EnsembleFormationException(Throwable cause) { + super(cause); + } + + public EnsembleFormationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/knowledge/container/KnowledgeContainerException.java b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/knowledge/container/KnowledgeContainerException.java index e79d36346..3cca37ad2 100644 --- a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/knowledge/container/KnowledgeContainerException.java +++ b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/knowledge/container/KnowledgeContainerException.java @@ -1,5 +1,7 @@ package cz.cuni.mff.d3s.deeco.knowledge.container; +import cz.cuni.mff.d3s.deeco.runtime.DEECoException; + /** * General exception thrown by the {@link TrackingKnowledgeContainer} class. * The exception message and the inner exception contain the details. @@ -11,16 +13,13 @@ * * @see TrackingKnowledgeContainer */ -public class KnowledgeContainerException extends Exception { +public class KnowledgeContainerException extends DEECoException { /** * */ private static final long serialVersionUID = -9219992641061413076L; - - public KnowledgeContainerException() { - } - + public KnowledgeContainerException(String message) { super(message); } @@ -31,11 +30,5 @@ public KnowledgeContainerException(Throwable cause) { public KnowledgeContainerException(String message, Throwable cause) { super(message, cause); - } - - public KnowledgeContainerException(String message, Throwable cause, - boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - + } } diff --git a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/ComponentInstanceRecord.java b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/ComponentInstanceRecord.java index d5de4d951..6061c16b9 100644 --- a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/ComponentInstanceRecord.java +++ b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/ComponentInstanceRecord.java @@ -69,6 +69,10 @@ public Map getEnsembleTasks() { return ensembleTasks; } + /** + * Gets all ensemble formation tasks associated with this instance. + * @return Associated ensemble formation tasks. + */ public List getEnsembleFormationTasks() { return ensembleFormationTasks; } diff --git a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/DEECoContainer.java b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/DEECoContainer.java index 488c331a3..05dc6aa93 100644 --- a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/DEECoContainer.java +++ b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/DEECoContainer.java @@ -57,7 +57,7 @@ public interface DEECoContainer { public ComponentInstance deployComponent(Object components) throws AnnotationProcessorException; /** - * Deploys ensembles to the DEECo rutime by parsing them and adding them to the metadata model. + * Deploys ensembles to the DEECo runtime by parsing them and adding them to the metadata model. * As soon as they are added to the model, ensembles are dynamically deployed (relevant tasks are created and scheduled). * To be used by plugins to deploy "system ensembles" that specify knowledge exchange between "system components" and are scheduled along with application ensembles. * @@ -70,6 +70,11 @@ public interface DEECoContainer { void undeployEnsemble(String ensembleName) throws AnnotationProcessorException, DuplicateEnsembleDefinitionException; + /** + * Deploys an ensemble factory to this DEECo container, immediately registering it with the runtime and creating and scheduling the relevant tasks. + * Note that unlike the {@link #deployEnsemble} method, no information is added to the metadata model. + * @param factory {@link EnsembleFactory} implementor used for specific ensemble formation. + */ public void deployEnsembleFactory(EnsembleFactory factory); /** diff --git a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/DEECoNode.java b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/DEECoNode.java index 616f08b58..e56cfd1e5 100644 --- a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/DEECoNode.java +++ b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/DEECoNode.java @@ -218,6 +218,7 @@ public int getId() { return nodeId; } + @Override public ComponentInstance deployComponent(Object component) throws AnnotationProcessorException { return processor.processComponent(component); } @@ -227,6 +228,7 @@ public EnsembleDefinition deployEnsemble(Class ensemble) throws AnnotationProces return processor.processEnsemble(ensemble); } + @Override public void undeployEnsemble(String ensembleName) throws AnnotationProcessorException, DuplicateEnsembleDefinitionException { processor.removeEnsemble(ensembleName); } diff --git a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/RuntimeFrameworkImpl.java b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/RuntimeFrameworkImpl.java index c68c81c9c..96c0cd4a9 100644 --- a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/RuntimeFrameworkImpl.java +++ b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/runtime/RuntimeFrameworkImpl.java @@ -321,11 +321,19 @@ public void notifyChanged(Notification notification) { instance.eAdapters().add(ensembleControllerAdapter); componentInstanceAdapters.put(instance, ensembleControllerAdapter); - } + } + /** + * Registers the provided factory with this runtime instance. An {@link EnsembleFormationTask} is immediately created + * for each existing component and associated with it. The factory is stored in the runtime, and an additional task + * is created whenever a new component is added. + */ @Override public void registerEnsembleFactory(EnsembleFactory factory) { - // TODO Guards + if (factory == null) + throw new IllegalArgumentException("Attempted to pass a null factory argument."); + if (registeredEnsembleFactories.contains(factory)) + throw new IllegalStateException("Cannot register the same factory object twice"); for(ComponentInstance instance : componentRecords.keySet()) { EnsembleFormationTask newTask = new EnsembleFormationTask(scheduler, factory, instance); diff --git a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/task/EnsembleFormationTask.java b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/task/EnsembleFormationTask.java index 9a0a843fe..b0f236b1f 100644 --- a/jdeeco-core/src/cz/cuni/mff/d3s/deeco/task/EnsembleFormationTask.java +++ b/jdeeco-core/src/cz/cuni/mff/d3s/deeco/task/EnsembleFormationTask.java @@ -3,6 +3,7 @@ import java.util.Collection; import cz.cuni.mff.d3s.deeco.ensembles.EnsembleFactory; +import cz.cuni.mff.d3s.deeco.ensembles.EnsembleFormationException; import cz.cuni.mff.d3s.deeco.ensembles.EnsembleInstance; import cz.cuni.mff.d3s.deeco.knowledge.container.KnowledgeContainer; import cz.cuni.mff.d3s.deeco.knowledge.container.KnowledgeContainerException; @@ -13,35 +14,48 @@ import cz.cuni.mff.d3s.deeco.model.runtime.custom.TimeTriggerExt; import cz.cuni.mff.d3s.deeco.scheduler.Scheduler; +/** + * Represents a task responsible for periodically forming ensemble instances (i.e. implementors of {@link EnsembleInstance}) using + * the associated {@link EnsembleFactory} and performing knowledge exchange on the resultant instances. Uses {@link TrackingKnowledgeContainer} + * for providing knowledge access to the ensembles, as well as committing the changes back into the knowledge storage. + * + * @author Filip Krijt + */ public class EnsembleFormationTask extends Task { private EnsembleFactory factory; private ComponentInstance componentInstance; private TimeTrigger trigger; + /** + * Creates a new {@link EnsembleFormationTask} associated with the provided factory and component instance. + * @param scheduler + * @param factory + * @param componentInstance + */ public EnsembleFormationTask(Scheduler scheduler, EnsembleFactory factory, ComponentInstance componentInstance) { super(scheduler); this.factory = factory; this.componentInstance = componentInstance; this.trigger = new TimeTriggerExt(); - this.trigger.setOffset(factory.getOffset()); - this.trigger.setPeriod(factory.getPeriod()); + this.trigger.setOffset(factory.getSchedulingOffset()); + this.trigger.setPeriod(factory.getSchedulingPeriod()); } @Override - public void invoke(Trigger trigger) throws TaskInvocationException { - KnowledgeContainer container = TrackingKnowledgeContainer.createFromKnowledgeManagers(componentInstance.getKnowledgeManager(), - componentInstance.getShadowKnowledgeManagerRegistry().getShadowKnowledgeManagers()); - - Collection instances = factory.createInstances(container); - - for(EnsembleInstance instance : instances) { - instance.performKnowledgeExchange(); - } + public void invoke(Trigger trigger) throws TaskInvocationException { try { + KnowledgeContainer container = TrackingKnowledgeContainer.createFromKnowledgeManagers(componentInstance.getKnowledgeManager(), + componentInstance.getShadowKnowledgeManagerRegistry().getShadowKnowledgeManagers()); + + Collection instances = factory.createInstances(container); + + for(EnsembleInstance instance : instances) { + instance.performKnowledgeExchange(); + } container.commitChanges(); - } catch (KnowledgeContainerException e) { + } catch (KnowledgeContainerException | EnsembleFormationException e) { throw new TaskInvocationException(e); } } @@ -59,5 +73,9 @@ protected void unregisterTriggers() { @Override public TimeTrigger getTimeTrigger() { return trigger; - } + } + + public EnsembleFactory getFactory() { + return factory; + } } diff --git a/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/DummyEnsemblesTest.java b/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/DummyEnsemblesTest.java new file mode 100644 index 000000000..8a416cd87 --- /dev/null +++ b/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/DummyEnsemblesTest.java @@ -0,0 +1,84 @@ +package cz.cuni.mff.d3s.deeco.demo.intelligent; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; +import cz.cuni.mff.d3s.deeco.annotations.processor.AnnotationProcessorException; +import cz.cuni.mff.d3s.deeco.ensembles.EnsembleFactory; +import cz.cuni.mff.d3s.deeco.runners.DEECoSimulation; +import cz.cuni.mff.d3s.deeco.runtime.DEECoException; +import cz.cuni.mff.d3s.deeco.runtime.DEECoNode; +import cz.cuni.mff.d3s.deeco.timer.DiscreteEventTimer; +import cz.cuni.mff.d3s.deeco.timer.SimulationTimer; + +/** + * A demo of the {@link EnsembleFactory}-oriented ensemble deployment functionality. Also serves as an integration test verifying + * that ensemble formation and subsequent knowledge transfer is performed. + * @author Filip Krijt + * + */ +public class DummyEnsemblesTest { + + public static void main(String[] args) throws InstantiationException, IllegalAccessException, DEECoException, AnnotationProcessorException { + new DummyEnsemblesTest().testEnsembles(false); + } + + @Test + public void testEnsembles() throws InstantiationException, IllegalAccessException, DEECoException, AnnotationProcessorException { + testEnsembles(true); + } + + private void testEnsembles(boolean silent) throws InstantiationException, IllegalAccessException, DEECoException, AnnotationProcessorException { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if (silent) { + SimpleEnsembleFactory.outputStream = new PrintStream(baos); + } else { + SimpleEnsembleFactory.outputStream = System.out; + } + + /* create main application container */ + SimulationTimer simulationTimer = new DiscreteEventTimer(); + DEECoSimulation realm = new DEECoSimulation(simulationTimer); + + /* create one and only deeco node (centralized deployment) */ + DEECoNode deeco = realm.createNode(0); + /* deploy components and an ensemble factory */ + + SimpleEnsembleFactory factory = new SimpleEnsembleFactory(); + + List robots = new ArrayList(Arrays.asList(new Robot("Wall-E"), new Robot("Gizmo"), new Robot("R2"))); + + for(Robot r : robots) { + deeco.deployComponent(r); + } + + deeco.deployEnsembleFactory(factory); + + // Add another component for testing purposes once the factory is deployed + Robot omnius = new Robot("Omnius"); + robots.add(omnius); + deeco.deployComponent(omnius); + + // Run the simulation + realm.start(999); + + if (silent) { + String out = baos.toString().trim(); + // Formation should be requested for each robot as many times as the formation period fits in the simulation run time + int scheduleCount = robots.size() * ((999 - factory.getSchedulingOffset()) / factory.getSchedulingPeriod() + 1); + assertEquals(scheduleCount, factory.formationCount); + + // The output should be strictly alternating between formation and exchange, with exactly scheduleCount occurences + String desiredOutput = String.join(System.lineSeparator(), Collections.nCopies(scheduleCount, + "Ensemble formation requested!" + System.lineSeparator() + "Knowledge exchange performed!")); + + assertEquals(desiredOutput, out); + } + } +} diff --git a/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/SimpleEnsemble.java b/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/SimpleEnsemble.java index e7d1eba2d..3aa3c3ff3 100644 --- a/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/SimpleEnsemble.java +++ b/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/SimpleEnsemble.java @@ -6,6 +6,6 @@ public class SimpleEnsemble implements EnsembleInstance { @Override public void performKnowledgeExchange() { - //System.out.println("Knowledge ex!"); + SimpleEnsembleFactory.outputStream.println("Knowledge exchange performed!"); } } diff --git a/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/SimpleEnsembleFactory.java b/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/SimpleEnsembleFactory.java index e36e205a6..256339599 100644 --- a/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/SimpleEnsembleFactory.java +++ b/jdeeco-core/test/cz/cuni/mff/d3s/deeco/demo/intelligent/SimpleEnsembleFactory.java @@ -1,5 +1,6 @@ package cz.cuni.mff.d3s.deeco.demo.intelligent; +import java.io.PrintStream; import java.util.Arrays; import java.util.Collection; @@ -8,20 +9,23 @@ import cz.cuni.mff.d3s.deeco.knowledge.container.KnowledgeContainer; public class SimpleEnsembleFactory implements EnsembleFactory { - + public static PrintStream outputStream = System.out; + int formationCount = 0; + @Override - public Collection createInstances(KnowledgeContainer container) { - //System.out.println("Formation requested!"); + public Collection createInstances(KnowledgeContainer container) { + ++formationCount; + outputStream.println("Ensemble formation requested!"); return Arrays.asList(new SimpleEnsemble()); } @Override - public int getOffset() { + public int getSchedulingOffset() { return 42; } @Override - public int getPeriod() { - return 1000; + public int getSchedulingPeriod() { + return 400; } } diff --git a/jdeeco-core/test/cz/cuni/mff/d3s/deeco/runtime/ComponentInstanceRecordTest.java b/jdeeco-core/test/cz/cuni/mff/d3s/deeco/runtime/ComponentInstanceRecordTest.java index 7a3b70a14..ded037121 100644 --- a/jdeeco-core/test/cz/cuni/mff/d3s/deeco/runtime/ComponentInstanceRecordTest.java +++ b/jdeeco-core/test/cz/cuni/mff/d3s/deeco/runtime/ComponentInstanceRecordTest.java @@ -11,6 +11,7 @@ import cz.cuni.mff.d3s.deeco.model.runtime.api.ComponentInstance; import cz.cuni.mff.d3s.deeco.model.runtime.api.ComponentProcess; import cz.cuni.mff.d3s.deeco.model.runtime.api.EnsembleController; +import cz.cuni.mff.d3s.deeco.task.EnsembleFormationTask; import cz.cuni.mff.d3s.deeco.task.Task; /** @@ -44,6 +45,7 @@ public void testGetAllTasks() { put(mock(ComponentProcess.class), pt1); put(mock(ComponentProcess.class), pt2); }}); + // AND a non empty list of ensemble tasks final Task et1 = mock(Task.class); final Task et2 = mock(Task.class); @@ -51,13 +53,17 @@ public void testGetAllTasks() { put(mock(EnsembleController.class), et1); put(mock(EnsembleController.class), et2); }}); + + // AND an ensemble formation task + final EnsembleFormationTask eft = mock(EnsembleFormationTask.class); + tested.getEnsembleFormationTasks().add(eft); + // THEN the getAllTasks() returns exactly all the tasks - assertEquals(4, tested.getAllTasks().size()); + assertEquals(5, tested.getAllTasks().size()); assertTrue(tested.getAllTasks().contains(pt1)); assertTrue(tested.getAllTasks().contains(pt2)); assertTrue(tested.getAllTasks().contains(et1)); assertTrue(tested.getAllTasks().contains(et2)); - + assertTrue(tested.getAllTasks().contains(eft)); } - } diff --git a/jdeeco-core/test/cz/cuni/mff/d3s/deeco/runtime/RuntimeFrameworkImplTest.java b/jdeeco-core/test/cz/cuni/mff/d3s/deeco/runtime/RuntimeFrameworkImplTest.java index 0d65b30da..d2651930e 100644 --- a/jdeeco-core/test/cz/cuni/mff/d3s/deeco/runtime/RuntimeFrameworkImplTest.java +++ b/jdeeco-core/test/cz/cuni/mff/d3s/deeco/runtime/RuntimeFrameworkImplTest.java @@ -16,6 +16,7 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import cz.cuni.mff.d3s.deeco.ensembles.EnsembleFactory; import cz.cuni.mff.d3s.deeco.executor.Executor; import cz.cuni.mff.d3s.deeco.integrity.RatingsManager; import cz.cuni.mff.d3s.deeco.knowledge.BaseKnowledgeManager; @@ -34,6 +35,7 @@ import cz.cuni.mff.d3s.deeco.model.runtime.meta.RuntimeMetadataFactory; import cz.cuni.mff.d3s.deeco.runtimelog.RuntimeLogger; import cz.cuni.mff.d3s.deeco.scheduler.Scheduler; +import cz.cuni.mff.d3s.deeco.task.EnsembleFormationTask; import cz.cuni.mff.d3s.deeco.task.Task; /** @@ -387,6 +389,84 @@ public void callbackCalledWhenComponentAddedToModel() { assertTrue(log.getLog().isEmpty()); } + /* ************************************************************************ + * registerEnsembleFactory + * ***********************************************************************/ + + + @Test (expected=IllegalArgumentException.class) + public void testRegisterEnsembleFactoryNullFactory() { + RuntimeFrameworkImpl tested = spy(new RuntimeFrameworkImpl(model, scheduler, executor, kmContainer, ratingsManager)); + + tested.registerEnsembleFactory(null); + } + + @Test (expected=IllegalStateException.class) + public void testRegisterEnsembleFactoryRepeatedCall() { + RuntimeFrameworkImpl tested = spy(new RuntimeFrameworkImpl(model, scheduler, executor, kmContainer, ratingsManager)); + + EnsembleFactory f = mock(EnsembleFactory.class); + + tested.registerEnsembleFactory(f); + tested.registerEnsembleFactory(f); + } + + @Test + public void testFormationTasksAddedForExistingComponents() { + RuntimeFrameworkImpl tested = spy(new RuntimeFrameworkImpl(model, scheduler, executor, kmContainer, ratingsManager)); + + // Add some components before registering the factory + tested.componentInstanceAdded((EcoreUtil.copy(component))); + tested.componentInstanceAdded((EcoreUtil.copy(component))); + + EnsembleFactory factory = mock(EnsembleFactory.class); + + tested.registerEnsembleFactory(factory); + + // Verify that all recorded components have a task for this factory + for (ComponentInstanceRecord record : tested.componentRecords.values()) { + assertTrue(record.getEnsembleFormationTasks().stream().anyMatch(t -> t.getFactory().equals(factory))); + } + } + + @Test + public void testFormationTaskAddedForNewComponent() { + RuntimeFrameworkImpl tested = spy(new RuntimeFrameworkImpl(model, scheduler, executor, kmContainer, ratingsManager)); + + // First register a factory and then add a component + EnsembleFactory factory = mock(EnsembleFactory.class); + tested.registerEnsembleFactory(factory); + + tested.componentInstanceAdded(EcoreUtil.copy(component)); + + // Verify that all recorded components have a task for this factory + for (ComponentInstanceRecord record : tested.componentRecords.values()) { + assertTrue(record.getEnsembleFormationTasks().stream().anyMatch(t -> t.getFactory().equals(factory))); + } + } + + @Test + public void testFormationTaskAdditionAndRemoval() { + Scheduler scheduler = mock(Scheduler.class); + RuntimeFrameworkImpl tested = spy(new RuntimeFrameworkImpl(model, scheduler, executor, kmContainer, ratingsManager)); + + EnsembleFactory factory = mock(EnsembleFactory.class); + + ComponentInstance c = EcoreUtil.copy(component); + + tested.componentInstanceAdded(c); + tested.componentInstanceAdded(EcoreUtil.copy(component)); + tested.registerEnsembleFactory(factory); + tested.componentInstanceAdded(EcoreUtil.copy(component)); + + verify(scheduler, times(3)).addTask(isA(EnsembleFormationTask.class)); + verify(scheduler, times(0)).removeTask(any()); + + tested.componentInstanceRemoved(c); + verify(scheduler, times(1)).removeTask(isA(EnsembleFormationTask.class)); + } + + /* ************************************************************************ * componentInstanceRemoved * ***********************************************************************/