/*
* 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 com.google.common.base.Preconditions.checkNotNull;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
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.mgmt.ManagementContext;
import org.apache.brooklyn.core.entity.EntityPredicates;
import org.apache.brooklyn.core.entity.trait.FailingEntity;
import org.apache.brooklyn.core.location.SimulatedLocation;
import org.apache.brooklyn.core.location.cloud.AbstractAvailabilityZoneExtension;
import org.apache.brooklyn.core.location.cloud.AvailabilityZoneExtension;
import org.apache.brooklyn.core.location.internal.LocationInternal;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.entity.group.DynamicCluster;
import org.apache.brooklyn.entity.group.zoneaware.ProportionalZoneFailureDetector;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.time.Duration;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.base.Predicate;
import com.google.common.base.Ticker;
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 DynamicClusterWithAvailabilityZonesTest extends BrooklynAppUnitTestSupport {
private DynamicCluster cluster;
private SimulatedLocation loc;
@BeforeMethod(alwaysRun=true)
@Override
public void setUp() throws Exception {
super.setUp();
cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
.configure(DynamicCluster.ENABLE_AVAILABILITY_ZONES, true)
.configure(DynamicCluster.INITIAL_SIZE, 0)
.configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(TestEntity.class)));
loc = mgmt.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class));
loc.addExtension(AvailabilityZoneExtension.class, new SimulatedAvailabilityZoneExtension(mgmt, loc, ImmutableList.of("zone1", "zone2", "zone3", "zone4")));
}
@Test
public void testPicksCorrectNumSubLocations() throws Exception {
((EntityLocal)cluster).config().set(DynamicCluster.NUM_AVAILABILITY_ZONES, 2);
cluster.start(ImmutableList.of(loc));
List<Location> subLocations = cluster.getAttribute(DynamicCluster.SUB_LOCATIONS);
List<String> subLocationNames = getLocationNames(subLocations);
assertEquals(subLocationNames, ImmutableList.of("zone1", "zone2"));
}
@Test
public void testPicksCorrectNamedSubLocations() throws Exception {
((EntityLocal)cluster).config().set(DynamicCluster.AVAILABILITY_ZONE_NAMES, ImmutableList.of("zone2", "zone4"));
cluster.start(ImmutableList.of(loc));
List<Location> subLocations = cluster.getAttribute(DynamicCluster.SUB_LOCATIONS);
List<String> subLocationNames = getLocationNames(subLocations);
assertEquals(subLocationNames, ImmutableList.of("zone2", "zone4"));
}
@Test
public void testSpreadsEntitiesAcrossZonesEvenly() throws Exception {
((EntityLocal)cluster).config().set(DynamicCluster.AVAILABILITY_ZONE_NAMES, ImmutableList.of("zone1", "zone2"));
cluster.start(ImmutableList.of(loc));
cluster.resize(4);
List<String> locsUsed = getLocationNames(getLocationsOf(cluster.getMembers()));
Asserts.assertEqualsIgnoringOrder(locsUsed, ImmutableList.of("zone1", "zone1", "zone2", "zone2"));
cluster.resize(2);
locsUsed = getLocationNames(getLocationsOf(cluster.getMembers()));
Asserts.assertEqualsIgnoringOrder(locsUsed, ImmutableList.of("zone1", "zone2"));
cluster.resize(0);
locsUsed = getLocationNames(getLocationsOf(cluster.getMembers()));
Asserts.assertEqualsIgnoringOrder(locsUsed, ImmutableList.of());
}
@Test
public void testReplacesEntityInSameZone() throws Exception {
((EntityLocal)cluster).config().set(DynamicCluster.AVAILABILITY_ZONE_NAMES, ImmutableList.of("zone1", "zone2"));
cluster.start(ImmutableList.of(loc));
cluster.resize(4);
List<String> locsUsed = getLocationNames(getLocationsOf(cluster.getMembers()));
Asserts.assertEqualsIgnoringOrder(locsUsed, ImmutableList.of("zone1", "zone1", "zone2", "zone2"));
String idToRemove = Iterables.getFirst(cluster.getMembers(), null).getId();
String idAdded = cluster.replaceMember(idToRemove);
locsUsed = getLocationNames(getLocationsOf(cluster.getMembers()));
Asserts.assertEqualsIgnoringOrder(locsUsed, ImmutableList.of("zone1", "zone1", "zone2", "zone2"));
assertNull(Iterables.find(cluster.getMembers(), EntityPredicates.idEqualTo(idToRemove), null));
assertNotNull(Iterables.find(cluster.getMembers(), EntityPredicates.idEqualTo(idAdded), null));
}
@Test
public void testAbandonsFailingZone() throws Exception {
final long startTime = System.nanoTime();
final AtomicLong currentTime = new AtomicLong(startTime);
Ticker ticker = new Ticker() {
@Override public long read() {
return currentTime.get();
}
};
Predicate<Object> failurePredicate = new Predicate<Object>() {
final Set<Integer> failFor = ImmutableSet.of(1, 2);
int callCount = 0;
@Override public boolean apply(Object input) {
return failFor.contains(callCount++);
}
};
((EntityLocal)cluster).config().set(DynamicCluster.ZONE_FAILURE_DETECTOR, new ProportionalZoneFailureDetector(2, Duration.ONE_HOUR, 0.9, ticker));
((EntityLocal)cluster).config().set(DynamicCluster.AVAILABILITY_ZONE_NAMES, ImmutableList.of("zone1", "zone2"));
((EntityLocal)cluster).config().set(DynamicCluster.MEMBER_SPEC, EntitySpec.create(FailingEntity.class)
.configure(FailingEntity.FAIL_ON_START_CONDITION, failurePredicate));
cluster.start(ImmutableList.of(loc));
cluster.resize(1);
String locUsed = Iterables.getOnlyElement(getLocationNames(getLocationsOf(cluster.getMembers())));
String otherLoc = (locUsed.equals("zone1") ? "zone2" : "zone1");
// This entity will fail; configured to give up on that zone after just two failure
DynamicClusterTest.resizeExpectingError(cluster, 2);
assertEquals(cluster.getCurrentSize(), (Integer)1);
DynamicClusterTest.resizeExpectingError(cluster, 2);
assertEquals(cluster.getCurrentSize(), (Integer)1);
cluster.resize(3);
assertEquals(cluster.getCurrentSize(), (Integer)3);
List<String> locsUsed = getLocationNames(getLocationsOf(cluster.getMembers()));
Asserts.assertEqualsIgnoringOrder(locsUsed, ImmutableList.of(locUsed, locUsed, locUsed));
// After waiting long enough, we'll be willing to try again in that zone
currentTime.set(startTime + TimeUnit.MILLISECONDS.toNanos(1000*60*60 +1));
cluster.resize(4);
assertEquals(cluster.getCurrentSize(), (Integer)4);
locsUsed = getLocationNames(getLocationsOf(cluster.getMembers()));
Asserts.assertEqualsIgnoringOrder(locsUsed, ImmutableList.of(locUsed, locUsed, locUsed, otherLoc));
}
protected List<String> getLocationNames(Iterable<? extends Location> locs) {
List<String> result = Lists.newArrayList();
for (Location subLoc : locs) {
result.add(subLoc.getDisplayName());
}
return result;
}
protected List<Location> getLocationsOf(Iterable<? extends Entity> entities) {
List<Location> result = Lists.newArrayList();
for (Entity entity : entities) {
result.add(Iterables.getOnlyElement(entity.getLocations()));
}
return result;
}
// TODO create two variants of this test, one using the Extension as below,
// and the other using a MultiLocation, ideally one specified as a string
public static class SimulatedAvailabilityZoneExtension extends AbstractAvailabilityZoneExtension implements AvailabilityZoneExtension {
private final SimulatedLocation loc;
private final List<String> subLocNames;
public SimulatedAvailabilityZoneExtension(ManagementContext managementContext, SimulatedLocation loc, List<String> subLocNames) {
super(managementContext);
this.loc = checkNotNull(loc, "loc");
this.subLocNames = subLocNames;
}
@Override
protected List<Location> doGetAllSubLocations() {
List<Location> result = Lists.newArrayList();
for (String subLocName : subLocNames) {
result.add(newSubLocation(loc, subLocName));
}
return result;
}
@Override
protected boolean isNameMatch(Location loc, Predicate<? super String> namePredicate) {
return namePredicate.apply(loc.getDisplayName());
}
protected SimulatedLocation newSubLocation(Location parent, String displayName) {
return managementContext.getLocationManager().createLocation(LocationSpec.create(SimulatedLocation.class)
.parent(parent)
.configure(((LocationInternal)parent).config().getBag().getAllConfig())
.displayName(displayName));
}
}
}