// Copyright 2016 Twitter. All rights reserved.
//
// Licensed 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 com.twitter.heron.scheduler;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.twitter.heron.api.generated.TopologyAPI;
import com.twitter.heron.proto.system.PackingPlans;
import com.twitter.heron.proto.system.PhysicalPlans;
import com.twitter.heron.scheduler.UpdateTopologyManager.ContainerDelta;
import com.twitter.heron.spi.common.Config;
import com.twitter.heron.spi.common.Key;
import com.twitter.heron.spi.packing.PackingPlan;
import com.twitter.heron.spi.packing.PackingPlanProtoSerializer;
import com.twitter.heron.spi.scheduler.IScalable;
import com.twitter.heron.spi.statemgr.IStateManager;
import com.twitter.heron.spi.statemgr.Lock;
import com.twitter.heron.spi.statemgr.SchedulerStateManagerAdaptor;
import com.twitter.heron.spi.utils.NetworkUtils;
import com.twitter.heron.spi.utils.PackingTestUtils;
import com.twitter.heron.spi.utils.TMasterUtils;
import com.twitter.heron.spi.utils.TopologyTests;
@RunWith(PowerMockRunner.class)
public class UpdateTopologyManagerTest {
private static final String TOPOLOGY_NAME = "topologyName";
private Set<PackingPlan.ContainerPlan> currentContainerPlan;
private Set<PackingPlan.ContainerPlan> proposedContainerPlan;
private Set<PackingPlan.ContainerPlan> expectedContainersToAdd;
private Set<PackingPlan.ContainerPlan> expectedContainersToRemove;
private PackingPlan proposedPacking;
private PackingPlans.PackingPlan currentProtoPlan;
private PackingPlans.PackingPlan proposedProtoPlan;
private TopologyAPI.Topology testTopology;
@Before
public void init() {
Integer[] instanceIndexA = new Integer[] {37, 48, 59};
Integer[] instanceIndexB = new Integer[] {17, 22};
currentContainerPlan = buildContainerSet(new Integer[] {1, 2, 3, 4}, instanceIndexA);
proposedContainerPlan = buildContainerSet(new Integer[] {1, 3, 5, 6}, instanceIndexB);
expectedContainersToAdd = buildContainerSet(new Integer[] {5, 6}, instanceIndexB);
expectedContainersToRemove = buildContainerSet(new Integer[] {2, 4}, instanceIndexA);
PackingPlanProtoSerializer serializer = new PackingPlanProtoSerializer();
PackingPlan currentPacking = new PackingPlan("current", currentContainerPlan);
proposedPacking = new PackingPlan("proposed", proposedContainerPlan);
currentProtoPlan = serializer.toProto(currentPacking);
proposedProtoPlan = serializer.toProto(proposedPacking);
testTopology = TopologyTests.createTopology(
TOPOLOGY_NAME, new com.twitter.heron.api.Config(), "spoutname", "boltname", 1, 1);
assertEquals(TopologyAPI.TopologyState.RUNNING, testTopology.getState());
}
private static Lock mockLock(boolean available) throws InterruptedException {
Lock lock = mock(Lock.class);
when(lock.tryLock(any(Long.class), any(TimeUnit.class))).thenReturn(available);
return lock;
}
private static SchedulerStateManagerAdaptor mockStateManager(TopologyAPI.Topology topology,
PackingPlans.PackingPlan packingPlan,
Lock lock) {
SchedulerStateManagerAdaptor stateManager = mock(SchedulerStateManagerAdaptor.class);
when(stateManager.getPhysicalPlan(TOPOLOGY_NAME))
.thenReturn(PhysicalPlans.PhysicalPlan.getDefaultInstance());
when(stateManager.getTopology(TOPOLOGY_NAME)).thenReturn(topology);
when(stateManager.getPackingPlan(eq(TOPOLOGY_NAME))).thenReturn(packingPlan);
when(stateManager.getLock(eq(TOPOLOGY_NAME), eq(IStateManager.LockName.UPDATE_TOPOLOGY)))
.thenReturn(lock);
return stateManager;
}
private static Config mockRuntime(SchedulerStateManagerAdaptor stateManager) {
Config runtime = mock(Config.class);
when(runtime.getStringValue(Key.TOPOLOGY_NAME)).thenReturn(TOPOLOGY_NAME);
when(runtime.get(Key.SCHEDULER_STATE_MANAGER_ADAPTOR)).thenReturn(stateManager);
return runtime;
}
private UpdateTopologyManager spyUpdateManager(SchedulerStateManagerAdaptor stateManager,
IScalable scheduler,
TopologyAPI.Topology updatedTopology) {
Config mockRuntime = mockRuntime(stateManager);
UpdateTopologyManager spyUpdateManager = spy(new UpdateTopologyManager(
mock(Config.class), mockRuntime, Optional.of(scheduler))
);
when(spyUpdateManager.getUpdatedTopology(TOPOLOGY_NAME, this.proposedPacking, stateManager))
.thenReturn(updatedTopology);
return spyUpdateManager;
}
@Test
public void testContainerDelta() {
ContainerDelta result = new ContainerDelta(currentContainerPlan, proposedContainerPlan);
assertNotNull(result);
assertEquals(expectedContainersToAdd, result.getContainersToAdd());
assertEquals(expectedContainersToRemove, result.getContainersToRemove());
}
/**
* Test scalable scheduler invocation
*/
@Test
@PrepareForTest(TMasterUtils.class)
public void requestsToAddAndRemoveContainers() throws Exception {
Lock lock = mockLock(true);
SchedulerStateManagerAdaptor mockStateMgr = mockStateManager(
testTopology, this.currentProtoPlan, lock);
IScalable mockScheduler = mock(IScalable.class);
UpdateTopologyManager spyUpdateManager =
spyUpdateManager(mockStateMgr, mockScheduler, testTopology);
PowerMockito.spy(TMasterUtils.class);
PowerMockito.doNothing().when(TMasterUtils.class, "sendToTMaster",
any(String.class), eq(TOPOLOGY_NAME),
eq(mockStateMgr), any(NetworkUtils.TunnelConfig.class));
// reactivation won't happen since topology state is still running due to mock state manager
PowerMockito.doNothing().when(TMasterUtils.class, "transitionTopologyState",
eq(TOPOLOGY_NAME), eq(TMasterUtils.TMasterCommand.ACTIVATE), eq(mockStateMgr),
eq(TopologyAPI.TopologyState.PAUSED), eq(TopologyAPI.TopologyState.RUNNING),
any(NetworkUtils.TunnelConfig.class));
spyUpdateManager.updateTopology(currentProtoPlan, proposedProtoPlan);
verify(spyUpdateManager).deactivateTopology(eq(mockStateMgr), eq(testTopology));
verify(spyUpdateManager).reactivateTopology(eq(mockStateMgr), eq(testTopology), eq(2));
verify(mockScheduler).addContainers(expectedContainersToAdd);
verify(mockScheduler).removeContainers(expectedContainersToRemove);
verify(lock).tryLock(any(Long.class), any(TimeUnit.class));
verify(lock).unlock();
PowerMockito.verifyStatic(times(1));
TMasterUtils.transitionTopologyState(eq(TOPOLOGY_NAME),
eq(TMasterUtils.TMasterCommand.DEACTIVATE), eq(mockStateMgr),
eq(TopologyAPI.TopologyState.RUNNING), eq(TopologyAPI.TopologyState.PAUSED),
any(NetworkUtils.TunnelConfig.class));
PowerMockito.verifyStatic(times(1));
TMasterUtils.transitionTopologyState(eq(TOPOLOGY_NAME),
eq(TMasterUtils.TMasterCommand.ACTIVATE), eq(mockStateMgr),
eq(TopologyAPI.TopologyState.PAUSED), eq(TopologyAPI.TopologyState.RUNNING),
any(NetworkUtils.TunnelConfig.class));
}
@Test(expected = ConcurrentModificationException.class)
public void testLockTaken() throws Exception {
SchedulerStateManagerAdaptor mockStateMgr = mockStateManager(
testTopology, this.currentProtoPlan, mockLock(false));
UpdateTopologyManager spyUpdateManager =
spyUpdateManager(mockStateMgr, mock(IScalable.class), testTopology);
spyUpdateManager.updateTopology(currentProtoPlan, proposedProtoPlan);
}
@Test
public void testUpdateTopology() {
Map<String, Integer> bolts = new HashMap<>();
bolts.put("bolt1", 1);
bolts.put("bolt7", 7);
Map<String, Integer> spouts = new HashMap<>();
spouts.put("spout3", 3);
spouts.put("spout5", 5);
TopologyAPI.Topology topology = TopologyTests.createTopology(
TOPOLOGY_NAME, new com.twitter.heron.api.Config(), spouts, bolts);
// assert that the initial config settings are as expected
assertParallelism(topology, spouts, bolts);
Map<String, Integer> boltUpdates = new HashMap<>();
boltUpdates.put("bolt1", 3);
boltUpdates.put("bolt7", 2);
Map<String, Integer> spoutUpdates = new HashMap<>();
spoutUpdates.put("spout3", 8);
Map<String, Integer> updates = new HashMap<>();
updates.putAll(boltUpdates);
updates.putAll(spoutUpdates);
// assert that the updated topology config settings are as expected
topology = UpdateTopologyManager.mergeTopology(topology, updates);
bolts.putAll(boltUpdates);
spouts.putAll(spoutUpdates);
assertParallelism(topology, spouts, bolts);
}
private void assertParallelism(TopologyAPI.Topology topology,
Map<String, Integer> expectedSpouts,
Map<String, Integer> expectedBolts) {
for (String boltName : expectedBolts.keySet()) {
String foundParallelism = null;
for (TopologyAPI.Bolt bolt : topology.getBoltsList()) {
foundParallelism = getParallelism(bolt.getComp(), boltName);
if (foundParallelism != null) {
break;
}
}
assertEquals(Integer.toString(expectedBolts.get(boltName)), foundParallelism);
}
for (String spoutName : expectedSpouts.keySet()) {
String foundParallelism = null;
for (TopologyAPI.Spout spout : topology.getSpoutsList()) {
foundParallelism = getParallelism(spout.getComp(), spoutName);
if (foundParallelism != null) {
break;
}
}
assertEquals(Integer.toString(expectedSpouts.get(spoutName)), foundParallelism);
}
}
private static String getParallelism(TopologyAPI.Component component, String componentName) {
if (component.getName().equals(componentName)) {
for (TopologyAPI.Config.KeyValue keyValue : component.getConfig().getKvsList()) {
if (keyValue.getKey().equals(com.twitter.heron.api.Config.TOPOLOGY_COMPONENT_PARALLELISM)) {
return keyValue.getValue();
}
}
}
return null;
}
private static Set<PackingPlan.ContainerPlan> buildContainerSet(Integer[] containerIds,
Integer[] instanceIndexes) {
Set<PackingPlan.ContainerPlan> containerPlan = new HashSet<>();
for (int containerId : containerIds) {
containerPlan.add(PackingTestUtils.testContainerPlan(containerId, instanceIndexes));
}
return containerPlan;
}
}