/* * 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.proxy; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import java.net.Inet4Address; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.EntityLocal; import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.location.LocationSpec; import org.apache.brooklyn.api.location.MachineLocation; import org.apache.brooklyn.api.location.MachineProvisioningLocation; import org.apache.brooklyn.api.location.NoMachinesAvailableException; import org.apache.brooklyn.api.sensor.AttributeSensor; 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.test.BrooklynAppUnitTestSupport; import org.apache.brooklyn.core.test.entity.TestEntity; import org.apache.brooklyn.core.test.entity.TestEntityImpl; import org.apache.brooklyn.entity.group.Cluster; import org.apache.brooklyn.entity.group.DynamicCluster; import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.collections.MutableSet; import org.apache.brooklyn.util.core.flags.SetFromFlag; import org.apache.brooklyn.util.exceptions.Exceptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.apache.brooklyn.location.byon.FixedListMachineProvisioningLocation; import org.apache.brooklyn.location.ssh.SshMachineLocation; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; public class AbstractControllerTest extends BrooklynAppUnitTestSupport { private static final Logger log = LoggerFactory.getLogger(AbstractControllerTest.class); FixedListMachineProvisioningLocation<?> loc; Cluster cluster; TrackingAbstractController controller; @BeforeMethod(alwaysRun = true) @Override public void setUp() throws Exception { super.setUp(); List<SshMachineLocation> machines = new ArrayList<SshMachineLocation>(); for (int i=1; i<=10; i++) { SshMachineLocation machine = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class) .configure("address", Inet4Address.getByName("1.1.1."+i))); machines.add(machine); } loc = mgmt.getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class) .configure("machines", machines)); cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class) .configure("initialSize", 0) .configure("factory", new ClusteredEntity.Factory())); controller = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class) .configure("serverPool", cluster) .configure("portNumberSensor", ClusteredEntity.HTTP_PORT) .configure("domain", "mydomain")); app.start(ImmutableList.of(loc)); } // Fixes bug where entity that wrapped an AS7 entity was never added to nginx because hostname+port // was set after service_up. Now we listen to those changes and reset the nginx pool when these // values change. @Test public void testUpdateCalledWhenChildHostnameAndPortChanges() throws Exception { TestEntity child = cluster.addChild(EntitySpec.create(TestEntity.class)); cluster.addMember(child); List<Collection<String>> u = Lists.newArrayList(controller.getUpdates()); assertTrue(u.isEmpty(), "expected no updates, but got "+u); child.sensors().set(Startable.SERVICE_UP, true); // TODO Ugly sleep to allow AbstractController to detect node having been added Thread.sleep(100); child.sensors().set(ClusteredEntity.HOSTNAME, "mymachine"); child.sensors().set(Attributes.SUBNET_HOSTNAME, "mymachine"); child.sensors().set(ClusteredEntity.HTTP_PORT, 1234); assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine:1234")); child.sensors().set(ClusteredEntity.HOSTNAME, "mymachine2"); child.sensors().set(Attributes.SUBNET_HOSTNAME, "mymachine2"); assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine2:1234")); child.sensors().set(ClusteredEntity.HTTP_PORT, 1235); assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine2:1235")); child.sensors().set(ClusteredEntity.HOSTNAME, null); child.sensors().set(Attributes.SUBNET_HOSTNAME, null); assertEventuallyExplicitAddressesMatch(ImmutableList.<String>of()); } @Test public void testUpdateCalledWithAddressesOfNewChildren() { // First child cluster.resize(1); EntityLocal child = (EntityLocal) Iterables.getOnlyElement(cluster.getMembers()); List<Collection<String>> u = Lists.newArrayList(controller.getUpdates()); assertTrue(u.isEmpty(), "expected empty list but got "+u); child.sensors().set(ClusteredEntity.HTTP_PORT, 1234); child.sensors().set(Startable.SERVICE_UP, true); assertEventuallyAddressesMatchCluster(); // Second child cluster.resize(2); Asserts.succeedsEventually(new Runnable() { @Override public void run() { assertEquals(cluster.getMembers().size(), 2); }}); EntityLocal child2 = (EntityLocal) Iterables.getOnlyElement(MutableSet.builder().addAll(cluster.getMembers()).remove(child).build()); child2.sensors().set(ClusteredEntity.HTTP_PORT, 1234); child2.sensors().set(Startable.SERVICE_UP, true); assertEventuallyAddressesMatchCluster(); // And remove all children; expect all addresses to go away cluster.resize(0); assertEventuallyAddressesMatchCluster(); } @Test(groups = "Integration", invocationCount=10) public void testUpdateCalledWithAddressesOfNewChildrenManyTimes() { testUpdateCalledWithAddressesOfNewChildren(); } @Test public void testUpdateCalledWithAddressesRemovedForStoppedChildren() { // Get some children, so we can remove one... cluster.resize(2); for (Entity it: cluster.getMembers()) { ((EntityLocal)it).sensors().set(ClusteredEntity.HTTP_PORT, 1234); ((EntityLocal)it).sensors().set(Startable.SERVICE_UP, true); } assertEventuallyAddressesMatchCluster(); // Now remove one child cluster.resize(1); assertEquals(cluster.getMembers().size(), 1); assertEventuallyAddressesMatchCluster(); } @Test public void testUpdateCalledWithAddressesRemovedForServiceDownChildrenThatHaveClearedHostnamePort() { // Get some children, so we can remove one... cluster.resize(2); for (Entity it: cluster.getMembers()) { ((EntityLocal)it).sensors().set(ClusteredEntity.HTTP_PORT, 1234); ((EntityLocal)it).sensors().set(Startable.SERVICE_UP, true); } assertEventuallyAddressesMatchCluster(); // Now unset host/port, and remove children // Note the unsetting of hostname is done in SoftwareProcessImpl.stop(), so this is realistic for (Entity it : cluster.getMembers()) { ((EntityLocal)it).sensors().set(ClusteredEntity.HTTP_PORT, null); ((EntityLocal)it).sensors().set(ClusteredEntity.HOSTNAME, null); ((EntityLocal)it).sensors().set(Startable.SERVICE_UP, false); } assertEventuallyAddressesMatch(ImmutableList.<Entity>of()); } @Test public void testUsesHostAndPortSensor() throws Exception { controller = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class) .configure("serverPool", cluster) .configure("hostAndPortSensor", ClusteredEntity.HOST_AND_PORT) .configure("domain", "mydomain")); controller.start(Arrays.asList(loc)); TestEntity child = cluster.addChild(EntitySpec.create(TestEntity.class)); cluster.addMember(child); List<Collection<String>> u = Lists.newArrayList(controller.getUpdates()); assertTrue(u.isEmpty(), "expected no updates, but got "+u); child.sensors().set(Startable.SERVICE_UP, true); // TODO Ugly sleep to allow AbstractController to detect node having been added Thread.sleep(100); child.sensors().set(ClusteredEntity.HOST_AND_PORT, "mymachine:1234"); assertEventuallyExplicitAddressesMatch(ImmutableList.of("mymachine:1234")); } @Test public void testFailsIfSetHostAndPortAndHostnameOrPortNumberSensor() throws Exception { try { TrackingAbstractController controller2 = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class) .configure("serverPool", cluster) .configure("hostAndPortSensor", ClusteredEntity.HOST_AND_PORT) .configure("hostnameSensor", ClusteredEntity.HOSTNAME) .configure("domain", "mydomain")); controller2.start(Arrays.asList(loc)); } catch (Exception e) { IllegalStateException unwrapped = Exceptions.getFirstThrowableOfType(e, IllegalStateException.class); if (unwrapped != null && unwrapped.toString().contains("Must not set Sensor")) { // success } else { throw e; } } try { TrackingAbstractController controller3 = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class) .configure("serverPool", cluster) .configure("hostAndPortSensor", ClusteredEntity.HOST_AND_PORT) .configure("portNumberSensor", ClusteredEntity.HTTP_PORT) .configure("domain", "mydomain")); controller3.start(Arrays.asList(loc)); } catch (Exception e) { IllegalStateException unwrapped = Exceptions.getFirstThrowableOfType(e, IllegalStateException.class); if (unwrapped != null && unwrapped.toString().contains("Must not set Sensor")) { // success } else { throw e; } } } // Manual visual inspection test. Previously it repeatedly logged: // Unable to construct hostname:port representation for TestEntityImpl{id=jzwSBRQ2} (null:null); skipping in TrackingAbstractControllerImpl{id=tOn4k5BA} // every time the service-up was set to true again. @Test public void testMemberWithoutHostAndPortDoesNotLogErrorRepeatedly() throws Exception { controller = app.createAndManageChild(EntitySpec.create(TrackingAbstractController.class) .configure("serverPool", cluster) .configure("domain", "mydomain")); controller.start(ImmutableList.of(loc)); TestEntity child = app.createAndManageChild(EntitySpec.create(TestEntity.class)); cluster.addMember(child); for (int i = 0; i < 100; i++) { child.sensors().set(Attributes.SERVICE_UP, true); } Thread.sleep(100); List<Collection<String>> u = Lists.newArrayList(controller.getUpdates()); assertTrue(u.isEmpty(), "expected no updates, but got "+u); } private void assertEventuallyAddressesMatchCluster() { assertEventuallyAddressesMatch(cluster.getMembers()); } private void assertEventuallyAddressesMatch(final Collection<Entity> expectedMembers) { Asserts.succeedsEventually(MutableMap.of("timeout", 15000), new Runnable() { @Override public void run() { assertAddressesMatch(locationsToAddresses(1234, expectedMembers)); }} ); } private void assertEventuallyExplicitAddressesMatch(final Collection<String> expectedAddresses) { Asserts.succeedsEventually(MutableMap.of("timeout", 15000), new Runnable() { @Override public void run() { assertAddressesMatch(expectedAddresses); }} ); } private void assertAddressesMatch(final Collection<String> expectedAddresses) { List<Collection<String>> u = Lists.newArrayList(controller.getUpdates()); Collection<String> last = Iterables.getLast(u, null); log.debug("test "+u.size()+" updates, expecting "+expectedAddresses+"; actual "+last); assertTrue(u.size() > 0); assertEquals(ImmutableSet.copyOf(last), ImmutableSet.copyOf(expectedAddresses), "actual="+last+" expected="+expectedAddresses); assertEquals(last.size(), expectedAddresses.size(), "actual="+last+" expected="+expectedAddresses); } private Collection<String> locationsToAddresses(int port, Collection<Entity> entities) { Set<String> result = MutableSet.of(); for (Entity e: entities) { result.add( ((SshMachineLocation) e.getLocations().iterator().next()) .getAddress().getHostName()+":"+port); } return result; } public static class ClusteredEntity extends TestEntityImpl { public static class Factory implements EntityFactory<ClusteredEntity> { @Override public ClusteredEntity newEntity(Map flags, Entity parent) { return new ClusteredEntity(flags, parent); } } public ClusteredEntity(Map flags, Entity parent) { super(flags,parent); } public ClusteredEntity(Entity parent) { super(MutableMap.of(),parent); } public ClusteredEntity(Map flags) { super(flags,null); } public ClusteredEntity() { super(MutableMap.of(),null); } @SetFromFlag("hostname") public static final AttributeSensor<String> HOSTNAME = Attributes.HOSTNAME; @SetFromFlag("port") public static final AttributeSensor<Integer> HTTP_PORT = Attributes.HTTP_PORT; @SetFromFlag("hostAndPort") public static final AttributeSensor<String> HOST_AND_PORT = Attributes.HOST_AND_PORT; MachineProvisioningLocation provisioner; public void start(Collection<? extends Location> locs) { provisioner = (MachineProvisioningLocation) locs.iterator().next(); MachineLocation machine; try { machine = provisioner.obtain(MutableMap.of()); } catch (NoMachinesAvailableException e) { throw Exceptions.propagate(e); } addLocations(Arrays.asList(machine)); sensors().set(HOSTNAME, machine.getAddress().getHostName()); sensors().set(Attributes.SUBNET_HOSTNAME, machine.getAddress().getHostName()); } public void stop() { if (provisioner!=null) provisioner.release((MachineLocation) firstLocation()); } } }