/*
* 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.entity.group;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.entity.Group;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.factory.EntityFactory;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.core.location.PortRanges;
import org.apache.brooklyn.core.location.SimulatedLocation;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.core.test.entity.BlockingEntity;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.entity.stock.BasicEntity;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.repeat.Repeater;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
public class DynamicFabricTest extends BrooklynAppUnitTestSupport {
private static final Logger log = LoggerFactory.getLogger(DynamicFabricTest.class);
private static final int TIMEOUT_MS = 5*1000;
private Location loc1;
private Location loc2;
private Location loc3;
@BeforeMethod(alwaysRun=true)
@Override
public void setUp() throws Exception {
super.setUp();
loc1 = new SimulatedLocation();
loc2 = new SimulatedLocation();
loc3 = new SimulatedLocation();
}
@Test
public void testDynamicFabricUsesMemberSpecToCreateAndStartEntityWhenGivenSingleLocation() throws Exception {
runWithEntitySpecWithLocations(ImmutableList.of(loc1));
}
@Test
public void testDynamicFabricUsesMemberSpecToCreateAndStartsEntityWhenGivenManyLocations() throws Exception {
runWithEntitySpecWithLocations(ImmutableList.of(loc1,loc2,loc3));
}
private void runWithEntitySpecWithLocations(Collection<Location> locs) {
Collection<Location> unclaimedLocs = Lists.newArrayList(locs);
DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure("memberSpec", EntitySpec.create(TestEntity.class)));
app.start(locs);
assertEquals(fabric.getChildren().size(), locs.size(), Joiner.on(",").join(fabric.getChildren()));
assertEquals(fabric.getMembers().size(), locs.size(), "members="+fabric.getMembers());
assertEquals(ImmutableSet.copyOf(fabric.getMembers()), ImmutableSet.copyOf(fabric.getChildren()), "members="+fabric.getMembers()+"; children="+fabric.getChildren());
for (Entity it : fabric.getChildren()) {
TestEntity child = (TestEntity) it;
assertEquals(child.getCounter().get(), 1);
assertEquals(child.getLocations().size(), 1, Joiner.on(",").join(child.getLocations()));
assertTrue(unclaimedLocs.removeAll(child.getLocations()));
}
assertTrue(unclaimedLocs.isEmpty(), Joiner.on(",").join(unclaimedLocs));
}
@Test
public void testDynamicFabricCreatesAndStartsEntityWhenGivenSingleLocation() throws Exception {
runWithFactoryWithLocations(ImmutableList.of(loc1));
}
@Test
public void testDynamicFabricCreatesAndStartsEntityWhenGivenManyLocations() throws Exception {
runWithFactoryWithLocations(ImmutableList.of(loc1, loc2, loc3));
}
private void runWithFactoryWithLocations(Collection<Location> locs) {
Collection<Location> unclaimedLocs = Lists.newArrayList(locs);
DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure("factory", new EntityFactory<Entity>() {
@Override public Entity newEntity(Map flags, Entity parent) {
return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(TestEntity.class)
.parent(parent)
.configure(flags));
}}));
app.start(locs);
assertEquals(fabric.getChildren().size(), locs.size(), Joiner.on(",").join(fabric.getChildren()));
assertEquals(fabric.getMembers().size(), locs.size(), "members="+fabric.getMembers());
assertEquals(ImmutableSet.copyOf(fabric.getMembers()), ImmutableSet.copyOf(fabric.getChildren()), "members="+fabric.getMembers()+"; children="+fabric.getChildren());
for (Entity it : fabric.getChildren()) {
TestEntity child = (TestEntity) it;
assertEquals(child.getCounter().get(), 1);
assertEquals(child.getLocations().size(), 1, Joiner.on(",").join(child.getLocations()));
assertTrue(unclaimedLocs.removeAll(child.getLocations()));
}
assertTrue(unclaimedLocs.isEmpty(), Joiner.on(",").join(unclaimedLocs));
}
@Test
public void testNotifiesPostStartListener() throws Exception {
final List<Entity> entitiesAdded = new CopyOnWriteArrayList<Entity>();
DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure("factory", new EntityFactory<Entity>() {
@Override public Entity newEntity(Map flags, Entity parent) {
TestEntity result = app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(TestEntity.class)
.parent(parent)
.configure(flags));
entitiesAdded.add(result);
return result;
}}));
app.start(ImmutableList.of(loc1, loc2));
assertEquals(entitiesAdded.size(), 2);
assertEquals(ImmutableSet.copyOf(entitiesAdded), ImmutableSet.copyOf(fabric.getChildren()));
}
@Test
public void testSizeEnricher() throws Exception {
Collection<Location> locs = ImmutableList.of(loc1, loc2, loc3);
final DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure("factory", new EntityFactory<Entity>() {
@Override public Entity newEntity(Map fabricProperties, Entity parent) {
return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(DynamicCluster.class)
.parent(parent)
.configure("initialSize", 0)
.configure("factory", new EntityFactory<Entity>() {
@Override public Entity newEntity(Map clusterProperties, Entity parent) {
return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(TestEntity.class)
.parent(parent)
.configure(clusterProperties));
}}));
}}));
app.start(locs);
final AtomicInteger i = new AtomicInteger();
final AtomicInteger total = new AtomicInteger();
assertEquals(fabric.getChildren().size(), locs.size(), Joiner.on(",").join(fabric.getChildren()));
for (Entity it : fabric.getChildren()) {
Cluster child = (Cluster) it;
total.addAndGet(i.incrementAndGet());
child.resize(i.get());
}
Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
public void run() {
assertEquals(fabric.getAttribute(DynamicFabric.FABRIC_SIZE), (Integer) total.get());
assertEquals(fabric.getFabricSize(), (Integer) total.get());
}});
}
@Test
public void testDynamicFabricStartsEntitiesInParallel() throws Exception {
final List<CountDownLatch> latches = Lists.newCopyOnWriteArrayList();
DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure("factory", new EntityFactory<Entity>() {
@Override public Entity newEntity(Map flags, Entity parent) {
CountDownLatch latch = new CountDownLatch(1);
latches.add(latch);
return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(BlockingEntity.class)
.parent(parent)
.configure(flags)
.configure(BlockingEntity.STARTUP_LATCH, latch));
}}));
final Collection<Location> locs = ImmutableList.of(loc1, loc2);
final Task<?> task = fabric.invoke(Startable.START, ImmutableMap.of("locations", locs));
new Repeater("Wait until each task is executing")
.every(100, TimeUnit.MILLISECONDS)
.limitTimeTo(30, TimeUnit.SECONDS)
.until(new Callable<Boolean>() {
@Override public Boolean call() {
return latches.size() == locs.size();
}})
.runRequiringTrue();
assertFalse(task.isDone());
for (CountDownLatch latch : latches) {
latch.countDown();
}
new Repeater("Wait until complete")
.every(100, TimeUnit.MILLISECONDS)
.limitTimeTo(30, TimeUnit.SECONDS)
.until(new Callable<Boolean>() {
@Override public Boolean call() {
return task.isDone();
}})
.run();
assertEquals(fabric.getChildren().size(), locs.size(), Joiner.on(",").join(fabric.getChildren()));
for (Entity it : fabric.getChildren()) {
assertEquals(((TestEntity)it).getCounter().get(), 1);
}
}
@Test(groups="Integration")
public void testDynamicFabricStopsEntitiesInParallelManyTimes() throws Exception {
for (int i=0; i<100; i++) {
log.info("running testDynamicFabricStopsEntitiesInParallel iteration $i");
testDynamicFabricStopsEntitiesInParallel();
}
}
@Test
public void testDynamicFabricStopsEntitiesInParallel() throws Exception {
final List<CountDownLatch> shutdownLatches = Lists.newCopyOnWriteArrayList();
final List<CountDownLatch> executingShutdownNotificationLatches = Lists.newCopyOnWriteArrayList();
final DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure("factory", new EntityFactory<Entity>() {
@Override public Entity newEntity(Map flags, Entity parent) {
CountDownLatch shutdownLatch = new CountDownLatch(1);
CountDownLatch executingShutdownNotificationLatch = new CountDownLatch(1);
shutdownLatches.add(shutdownLatch);
executingShutdownNotificationLatches.add(executingShutdownNotificationLatch);
return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(BlockingEntity.class)
.parent(parent)
.configure(flags)
.configure(BlockingEntity.SHUTDOWN_LATCH, shutdownLatch)
.configure(BlockingEntity.EXECUTING_SHUTDOWN_NOTIFICATION_LATCH, executingShutdownNotificationLatch));
}}));
Collection<Location> locs = ImmutableList.of(loc1, loc2);
// Start the fabric (and check we have the required num things to concurrently stop)
fabric.start(locs);
assertEquals(shutdownLatches.size(), locs.size());
assertEquals(executingShutdownNotificationLatches.size(), locs.size());
assertEquals(fabric.getChildren().size(), locs.size());
Collection<Entity> children = fabric.getChildren();
// On stop, expect each child to get as far as blocking on its latch
final Task<?> task = fabric.invoke(Startable.STOP, ImmutableMap.<String,Object>of());
for (CountDownLatch it : executingShutdownNotificationLatches) {
assertTrue(it.await(10*1000, TimeUnit.MILLISECONDS));
}
assertFalse(task.isDone());
// When we release the latches, expect shutdown to complete
for (CountDownLatch latch : shutdownLatches) {
latch.countDown();
}
Asserts.succeedsEventually(MutableMap.of("timeout", 10*1000), new Runnable() {
public void run() {
assertTrue(task.isDone());
}});
Asserts.succeedsEventually(MutableMap.of("timeout", 10*1000), new Runnable() {
public void run() {
for (Entity it : fabric.getChildren()) {
int count = ((TestEntity)it).getCounter().get();
assertEquals(count, 0, it+" counter reports "+count);
}
}});
}
@Test
public void testDynamicFabricDoesNotAcceptUnstartableChildren() throws Exception {
DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure("factory", new EntityFactory<Entity>() {
@Override public Entity newEntity(Map flags, Entity parent) {
return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(BasicEntity.class)
.parent(parent)
.configure(flags));
}}));
try {
fabric.start(ImmutableList.of(loc1));
assertEquals(fabric.getChildren().size(), 1);
} catch (Exception e) {
Throwable unwrapped = Exceptions.getFirstInteresting(e);
if (unwrapped instanceof IllegalStateException && unwrapped.getMessage() != null && (unwrapped.getMessage().contains("is not Startable"))) {
// success
} else {
throw e;
}
}
}
// For follow-the-sun, a valid pattern is to associate the FollowTheSunModel as a child of the dynamic-fabric.
// Thus we have "unstoppable" entities. Let's be relaxed about it, rather than blowing up.
@Test
public void testDynamicFabricIgnoresExtraUnstoppableChildrenOnStop() throws Exception {
DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure("factory", new EntityFactory<Entity>() {
@Override public Entity newEntity(Map flags, Entity parent) {
return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(TestEntity.class)
.parent(parent)
.configure(flags));
}}));
fabric.start(ImmutableList.of(loc1));
BasicEntity extraChild = fabric.addChild(EntitySpec.create(BasicEntity.class));
fabric.stop();
}
@Test
public void testDynamicFabricPropagatesProperties() throws Exception {
final EntityFactory<Entity> entityFactory = new EntityFactory<Entity>() {
@Override public Entity newEntity(Map flags, Entity parent) {
return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(TestEntity.class)
.parent(parent)
.configure(flags)
.configure("b", "avail"));
}};
final EntityFactory<Entity> clusterFactory = new EntityFactory<Entity>() {
@Override public Entity newEntity(Map flags, Entity parent) {
return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(DynamicCluster.class)
.parent(parent)
.configure(flags)
.configure("initialSize", 1)
.configure("factory", entityFactory)
.configure("customChildFlags", ImmutableMap.of("fromCluster", "passed to base entity"))
.configure("a", "ignored"));
// FIXME What to do about overriding DynamicCluster to do customChildFlags?
// new DynamicClusterImpl(clusterProperties) {
// protected Map getCustomChildFlags() { [fromCluster: "passed to base entity"] }
}};
DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure("factory", clusterFactory)
.configure("customChildFlags", ImmutableMap.of("fromFabric", "passed to cluster but not base entity"))
.configure(Attributes.HTTP_PORT, PortRanges.fromInteger(1234))); // for inheritance by children (as a port range)
app.start(ImmutableList.of(loc1));
assertEquals(fabric.getChildren().size(), 1);
DynamicCluster child = (DynamicCluster) getChild(fabric, 0);
assertEquals(child.getMembers().size(), 1);
assertEquals(getMember(child, 0).getConfig(Attributes.HTTP_PORT.getConfigKey()), PortRanges.fromInteger(1234));
assertEquals(((TestEntity)getMember(child, 0)).getConfigureProperties().get("a"), null);
assertEquals(((TestEntity)getMember(child, 0)).getConfigureProperties().get("b"), "avail");
assertEquals(((TestEntity)getMember(child, 0)).getConfigureProperties().get("fromCluster"), "passed to base entity");
assertEquals(((TestEntity)getMember(child, 0)).getConfigureProperties().get("fromFabric"), null);
child.resize(2);
assertEquals(child.getMembers().size(), 2);
assertEquals(getGrandchild(fabric, 0, 1).getConfig(Attributes.HTTP_PORT.getConfigKey()), PortRanges.fromInteger(1234));
assertEquals(((TestEntity)getMember(child, 1)).getConfigureProperties().get("a"), null);
assertEquals(((TestEntity)getMember(child, 1)).getConfigureProperties().get("b"), "avail");
assertEquals(((TestEntity)getMember(child, 1)).getConfigureProperties().get("fromCluster"), "passed to base entity");
assertEquals(((TestEntity)getMember(child, 1)).getConfigureProperties().get("fromFabric"), null);
}
@Test
public void testExistingChildrenStarted() throws Exception {
List<Location> locs = ImmutableList.of(loc1, loc2, loc3);
DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure(DynamicFabric.MEMBER_SPEC, EntitySpec.create(TestEntity.class)));
List<TestEntity> existingChildren = Lists.newArrayList();
for (int i = 0; i < 3; i++) {
existingChildren.add(fabric.addChild(EntitySpec.create(TestEntity.class)));
}
app.start(locs);
// Expect only these existing children
Asserts.assertEqualsIgnoringOrder(fabric.getChildren(), existingChildren);
Asserts.assertEqualsIgnoringOrder(fabric.getMembers(), existingChildren);
// Expect one location per existing child
List<Location> remainingLocs = MutableList.copyOf(locs);
for (Entity existingChild : existingChildren) {
Collection<Location> childLocs = existingChild.getLocations();
assertEquals(childLocs.size(), 1, "childLocs="+childLocs);
assertTrue(remainingLocs.removeAll(childLocs));
}
}
@Test
public void testExistingChildrenStartedRoundRobiningAcrossLocations() throws Exception {
List<Location> locs = ImmutableList.of(loc1, loc2);
DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure(DynamicFabric.MEMBER_SPEC, EntitySpec.create(TestEntity.class)));
List<TestEntity> existingChildren = Lists.newArrayList();
for (int i = 0; i < 4; i++) {
existingChildren.add(fabric.addChild(EntitySpec.create(TestEntity.class)));
}
app.start(locs);
// Expect only these existing children
Asserts.assertEqualsIgnoringOrder(fabric.getChildren(), existingChildren);
Asserts.assertEqualsIgnoringOrder(fabric.getMembers(), existingChildren);
// Expect one location per existing child (round-robin)
// Expect one location per existing child
List<Location> remainingLocs = MutableList.<Location>builder().addAll(locs).addAll(locs).build();
for (Entity existingChild : existingChildren) {
Collection<Location> childLocs = existingChild.getLocations();
assertEquals(childLocs.size(), 1, "childLocs="+childLocs);
assertTrue(remainingLocs.remove(Iterables.get(childLocs, 0)), "childLocs="+childLocs+"; remainingLocs="+remainingLocs+"; allLocs="+locs);
}
}
@Test
public void testExistingChildrenToppedUpWhenNewMembersIfMoreLocations() throws Exception {
List<Location> locs = ImmutableList.of(loc1, loc2, loc3);
DynamicFabric fabric = app.createAndManageChild(EntitySpec.create(DynamicFabric.class)
.configure(DynamicFabric.MEMBER_SPEC, EntitySpec.create(TestEntity.class)));
TestEntity existingChild = fabric.addChild(EntitySpec.create(TestEntity.class));
app.start(locs);
// Expect three children: the existing one, and one per other location
assertEquals(fabric.getChildren().size(), 3, "children="+fabric.getChildren());
assertTrue(fabric.getChildren().contains(existingChild), "children="+fabric.getChildren()+"; existingChild="+existingChild);
Asserts.assertEqualsIgnoringOrder(fabric.getMembers(), fabric.getChildren());
List<Location> remainingLocs = MutableList.<Location>builder().addAll(locs).build();
for (Entity child : fabric.getChildren()) {
Collection<Location> childLocs = child.getLocations();
assertEquals(childLocs.size(), 1, "childLocs="+childLocs);
assertTrue(remainingLocs.remove(Iterables.get(childLocs, 0)), "childLocs="+childLocs+"; remainingLocs="+remainingLocs+"; allLocs="+locs);
}
}
private Entity getGrandchild(Entity entity, int childIndex, int grandchildIndex) {
Entity child = getChild(entity, childIndex);
return Iterables.get(child.getChildren(), grandchildIndex);
}
private Entity getChild(Entity entity, int childIndex) {
return Iterables.get(entity.getChildren(), childIndex);
}
private Entity getMember(Group entity, int memberIndex) {
return Iterables.get(entity.getMembers(), memberIndex);
}
}