/*
* 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.policy.followthesun;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.location.SimulatedLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.Test;
import org.apache.brooklyn.policy.loadbalancing.MockContainerEntity;
import org.apache.brooklyn.policy.loadbalancing.MockItemEntity;
import org.apache.brooklyn.policy.loadbalancing.Movable;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableMap;
import com.google.common.base.Function;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
public class FollowTheSunPolicyTest extends AbstractFollowTheSunPolicyTest {
private static final Logger LOG = LoggerFactory.getLogger(FollowTheSunPolicyTest.class);
@Test
public void testPolicyUpdatesModel() {
final MockContainerEntity containerA = newContainer(app, loc1, "A");
final MockItemEntity item1 = newItem(app, containerA, "1");
final MockItemEntity item2 = newItem(app, containerA, "2");
item2.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item1, 11d));
Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
@Override public void run() {
assertEquals(ImmutableSet.of(item1, item2), model.getItems());
assertEquals(model.getItemContainer(item1), containerA);
assertEquals(model.getItemLocation(item1), loc1);
assertEquals(model.getContainerLocation(containerA), loc1);
assertEquals(model.getDirectSendsToItemByLocation(), ImmutableMap.of(item2, ImmutableMap.of(loc1, 11d)));
}});
}
@Test
public void testPolicyAcceptsLocationFinder() {
pool.policies().remove(policy);
Function<Entity, Location> customLocationFinder = new Function<Entity, Location>() {
@Override public Location apply(Entity input) {
return new SimulatedLocation(MutableMap.of("name", "custom location for "+input));
}};
FollowTheSunPolicy customPolicy = new FollowTheSunPolicy(
MutableMap.of("minPeriodBetweenExecs", 0, "locationFinder", customLocationFinder),
MockItemEntity.ITEM_USAGE_METRIC,
model,
FollowTheSunParameters.newDefault());
pool.policies().add(customPolicy);
final MockContainerEntity containerA = newContainer(app, loc1, "A");
Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
@Override public void run() {
assertEquals(model.getContainerLocation(containerA).getDisplayName(), "custom location for "+containerA);
}});
}
@Test
public void testNoopBalancing() throws Exception {
// Set-up containers and items.
MockContainerEntity containerA = newContainer(app, loc1, "A");
MockContainerEntity containerB = newContainer(app, loc2, "B");
MockItemEntity item1 = newItem(app, containerA, "1", Collections.<Entity, Double>emptyMap());
MockItemEntity item2 = newItem(app, containerB, "2", Collections.<Entity, Double>emptyMap());
Thread.sleep(SHORT_WAIT_MS);
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
}
@Test
public void testMovesItemToFollowDemand() throws Exception {
// Set-up containers and items.
MockContainerEntity containerA = newContainer(app, loc1, "A");
MockContainerEntity containerB = newContainer(app, loc2, "B");
MockItemEntity item1 = newItem(app, containerA, "1");
MockItemEntity item2 = newItem(app, containerB, "2");
item1.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.<MockItemEntity>of(), containerB, ImmutableList.of(item1, item2)));
}
@Test
public void testNoopIfDemandIsTiny() throws Exception {
// Set-up containers and items.
MockContainerEntity containerA = newContainer(app, loc1, "A");
MockContainerEntity containerB = newContainer(app, loc2, "B");
MockItemEntity item1 = newItem(app, containerA, "1");
MockItemEntity item2 = newItem(app, containerB, "2");
item1.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 0.1d));
Thread.sleep(SHORT_WAIT_MS);
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
}
@Test
public void testNoopIfDemandIsSimilarToCurrentLocation() throws Exception {
// Set-up containers and items.
MockContainerEntity containerA = newContainer(app, loc1, "A");
MockContainerEntity containerB = newContainer(app, loc2, "B");
MockItemEntity item1 = newItem(app, containerA, "1");
MockItemEntity item2 = newItem(app, containerA, "2");
MockItemEntity item3 = newItem(app, containerB, "3");
item1.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d, item3, 100.1d));
Thread.sleep(SHORT_WAIT_MS);
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2), containerB, ImmutableList.of(item3)));
}
@Test
public void testMoveDecisionIgnoresDemandFromItself() throws Exception {
// Set-up containers and items.
MockContainerEntity containerA = newContainer(app, loc1, "A");
MockContainerEntity containerB = newContainer(app, loc2, "B");
MockItemEntity item1 = newItem(app, containerA, "1");
MockItemEntity item2 = newItem(app, containerB, "2");
item1.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item1, 100d));
item2.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
Thread.sleep(SHORT_WAIT_MS);
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
}
@Test
public void testItemRemovedCausesRecalculationOfOptimalLocation() {
// Set-up containers and items.
MockContainerEntity containerA = newContainer(app, loc1, "A");
MockContainerEntity containerB = newContainer(app, loc2, "B");
MockItemEntity item1 = newItem(app, containerA, "1");
MockItemEntity item2 = newItem(app, containerA, "2");
MockItemEntity item3 = newItem(app, containerB, "3");
item1.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d, item3, 1000d));
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item2), containerB, ImmutableList.of(item1, item3)));
item3.stop();
Entities.unmanage(item3);
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2), containerB, ImmutableList.<MockItemEntity>of()));
}
@Test
public void testItemMovedCausesRecalculationOfOptimalLocationForOtherItems() {
// Set-up containers and items.
MockContainerEntity containerA = newContainer(app, loc1, "A");
MockContainerEntity containerB = newContainer(app, loc2, "B");
MockItemEntity item1 = newItem(app, containerA, "1");
MockItemEntity item2 = newItem(app, containerA, "2");
MockItemEntity item3 = newItem(app, containerB, "3");
item1.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d, item3, 1000d));
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item2), containerB, ImmutableList.of(item1, item3)));
item3.move(containerA);
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2, item3), containerB, ImmutableList.<MockItemEntity>of()));
}
@Test
public void testImmovableItemIsNotMoved() {
MockContainerEntity containerA = newContainer(app, loc1, "A");
MockContainerEntity containerB = newContainer(app, loc2, "B");
MockItemEntity item1 = newLockedItem(app, containerA, "1");
MockItemEntity item2 = newItem(app, containerB, "2");
item1.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1), containerB, ImmutableList.of(item2)));
}
@Test
public void testImmovableItemContributesTowardsLoad() {
MockContainerEntity containerA = newContainer(app, loc1, "A");
MockContainerEntity containerB = newContainer(app, loc2, "B");
MockItemEntity item1 = newLockedItem(app, containerA, "1");
MockItemEntity item2 = newItem(app, containerA, "2");
item1.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item1, 100d));
assertItemDistributionEventually(ImmutableMap.of(containerA, ImmutableList.of(item1, item2), containerB, ImmutableList.<MockItemEntity>of()));
}
// Marked as "Acceptance" due to time-sensitive nature :-(
@Test(groups={"Integration", "Acceptance"}, invocationCount=20)
public void testRepeatedRespectsMinPeriodBetweenExecs() throws Exception {
testRespectsMinPeriodBetweenExecs();
}
@Test(groups="Integration")
public void testRespectsMinPeriodBetweenExecs() throws Exception {
// Failed in jenkins several times, e.g. with event times [2647, 2655] and [1387, 2001].
// Aled's guess is that there was a delay notifying the listener the first time
// (which happens async), causing the listener to be notified in rapid
// succession. The policy execs probably did happen with a 1000ms separation.
//
// Therefore try up to three times to see if we get the desired separation. If the
// minPeriodBetweenExecs wasn't being respected, we'd expect the default 100ms; this
// test would therefore hardly ever pass.
final int MAX_ATTEMPTS = 3;
final long minPeriodBetweenExecs = 1000;
final long timePrecision = 250;
pool.policies().remove(policy);
MockContainerEntity containerA = newContainer(app, loc1, "A");
MockContainerEntity containerB = newContainer(app, loc2, "B");
MockItemEntity item1 = newItem(app, containerA, "1");
MockItemEntity item2 = newItem(app, containerB, "2");
MockItemEntity item3 = newItem(app, containerA, "3");
FollowTheSunPolicy customPolicy = new FollowTheSunPolicy(
MutableMap.of("minPeriodBetweenExecs", minPeriodBetweenExecs),
MockItemEntity.ITEM_USAGE_METRIC,
model,
FollowTheSunParameters.newDefault());
pool.policies().add(customPolicy);
// Record times that things are moved, by lisening to the container sensor being set
final Stopwatch stopwatch = Stopwatch.createStarted();
final List<Long> eventTimes = Lists.newCopyOnWriteArrayList();
final Semaphore semaphore = new Semaphore(0);
app.subscriptions().subscribe(item1, Movable.CONTAINER, new SensorEventListener<Entity>() {
@Override public void onEvent(SensorEvent<Entity> event) {
long eventTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
LOG.info("Received {} at {}", event, eventTime);
eventTimes.add(eventTime);
semaphore.release();
}});
String errmsg = "";
for (int i = 0; i < MAX_ATTEMPTS; i++) {
// Set the workrate, causing the policy to move item1 to item2's location, and wait for it to happen
item1.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item2, 100d));
assertTrue(semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(item1.getAttribute(Movable.CONTAINER), containerB);
// now cause item1 to be moved to item3's location, and wait for it to happen
item1.sensors().set(MockItemEntity.ITEM_USAGE_METRIC, ImmutableMap.<Entity,Double>of(item3, 100d));
assertTrue(semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(item1.getAttribute(Movable.CONTAINER), containerA);
LOG.info("testRepeatedRespectsMinPeriodBetweenExecs event times: "+eventTimes);
assertEquals(eventTimes.size(), 2);
if (eventTimes.get(1) - eventTimes.get(0) > (minPeriodBetweenExecs-timePrecision)) {
return; // success
} else {
errmsg += eventTimes;
eventTimes.clear();
}
}
fail("Event times never had sufficient gap: "+errmsg);
}
}