/** * 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 org.apache.aurora.scheduler.state; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import com.google.common.base.Optional; 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.Iterators; import com.google.common.collect.PeekingIterator; import org.apache.aurora.common.testing.easymock.EasyMockTest; import org.apache.aurora.common.util.testing.FakeClock; import org.apache.aurora.gen.AssignedTask; import org.apache.aurora.gen.Attribute; import org.apache.aurora.gen.HostAttributes; import org.apache.aurora.gen.MaintenanceMode; import org.apache.aurora.gen.ScheduleStatus; import org.apache.aurora.gen.ScheduledTask; import org.apache.aurora.gen.TaskEvent; import org.apache.aurora.scheduler.TaskIdGenerator; import org.apache.aurora.scheduler.base.Query; import org.apache.aurora.scheduler.base.TaskTestUtil; import org.apache.aurora.scheduler.base.Tasks; import org.apache.aurora.scheduler.events.EventSink; import org.apache.aurora.scheduler.events.PubsubEvent; import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange; import org.apache.aurora.scheduler.events.PubsubEvent.TasksDeleted; import org.apache.aurora.scheduler.mesos.Driver; import org.apache.aurora.scheduler.resources.ResourceManager; import org.apache.aurora.scheduler.scheduling.RescheduleCalculator; import org.apache.aurora.scheduler.storage.AttributeStore; import org.apache.aurora.scheduler.storage.Storage; import org.apache.aurora.scheduler.storage.Storage.MutateWork.NoResult; import org.apache.aurora.scheduler.storage.db.DbUtil; import org.apache.aurora.scheduler.storage.entities.IAssignedTask; import org.apache.aurora.scheduler.storage.entities.IHostAttributes; import org.apache.aurora.scheduler.storage.entities.IScheduledTask; import org.apache.aurora.scheduler.storage.entities.ITaskConfig; import org.apache.mesos.v1.Protos.AgentID; import org.easymock.Capture; import org.easymock.EasyMock; import org.easymock.IArgumentMatcher; import org.junit.Before; import org.junit.Test; import static org.apache.aurora.gen.ScheduleStatus.ASSIGNED; import static org.apache.aurora.gen.ScheduleStatus.FAILED; import static org.apache.aurora.gen.ScheduleStatus.FINISHED; import static org.apache.aurora.gen.ScheduleStatus.INIT; import static org.apache.aurora.gen.ScheduleStatus.KILLED; import static org.apache.aurora.gen.ScheduleStatus.KILLING; import static org.apache.aurora.gen.ScheduleStatus.LOST; import static org.apache.aurora.gen.ScheduleStatus.PENDING; import static org.apache.aurora.gen.ScheduleStatus.RUNNING; import static org.apache.aurora.gen.ScheduleStatus.THROTTLED; import static org.apache.aurora.scheduler.resources.ResourceTestUtil.resetPorts; import static org.apache.aurora.scheduler.resources.ResourceType.PORTS; import static org.apache.aurora.scheduler.state.StateChangeResult.ILLEGAL; import static org.apache.aurora.scheduler.state.StateChangeResult.INVALID_CAS_STATE; import static org.apache.aurora.scheduler.state.StateChangeResult.NOOP; import static org.apache.aurora.scheduler.state.StateChangeResult.SUCCESS; import static org.easymock.EasyMock.capture; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; import static org.junit.Assert.assertEquals; public class StateManagerImplTest extends EasyMockTest { private static final IHostAttributes HOST_A = IHostAttributes.build( new HostAttributes( "hostA", ImmutableSet.of(new Attribute("zone", ImmutableSet.of("1a")))) .setSlaveId("slaveIdA") .setMode(MaintenanceMode.NONE)); private static final ITaskConfig NON_SERVICE_CONFIG = setIsService(TaskTestUtil.makeConfig(TaskTestUtil.JOB), false); private static final ITaskConfig SERVICE_CONFIG = setIsService(NON_SERVICE_CONFIG, true); private Driver driver; private TaskIdGenerator taskIdGenerator; private EventSink eventSink; private RescheduleCalculator rescheduleCalculator; private StateManagerImpl stateManager; private final FakeClock clock = new FakeClock(); private Storage storage; @Before public void setUp() throws Exception { taskIdGenerator = createMock(TaskIdGenerator.class); driver = createMock(Driver.class); eventSink = createMock(EventSink.class); rescheduleCalculator = createMock(RescheduleCalculator.class); // TODO(William Farner): Use a mocked storage. storage = DbUtil.createStorage(); stateManager = new StateManagerImpl( clock, driver, taskIdGenerator, eventSink, rescheduleCalculator); storage.write((NoResult.Quiet) storeProvider -> { AttributeStore.Mutable attributeStore = storeProvider.getAttributeStore(); attributeStore.saveHostAttributes(HOST_A); }); } private static class StateChangeMatcher implements IArgumentMatcher { private final String taskId; private final ScheduleStatus from; private final ScheduleStatus to; StateChangeMatcher(String taskId, ScheduleStatus from, ScheduleStatus to) { this.taskId = taskId; this.from = from; this.to = to; } @Override public boolean matches(Object argument) { if (!(argument instanceof TaskStateChange)) { return false; } TaskStateChange change = (TaskStateChange) argument; return taskId.equals(Tasks.id(change.getTask())) && from == change.getOldState().get() && to == change.getNewState(); } @Override public void appendTo(StringBuffer buffer) { buffer.append(taskId).append(" ").append(from).append("->").append(to); } } PubsubEvent matchStateChange(String task, ScheduleStatus from, ScheduleStatus to) { EasyMock.reportMatcher(new StateChangeMatcher(task, from, to)); return null; } private static class DeletedTasksMatcher implements IArgumentMatcher { private final Set<String> taskIds; DeletedTasksMatcher(String taskId, String... taskIds) { this.taskIds = ImmutableSet.<String>builder().add(taskId).add(taskIds).build(); } @Override public boolean matches(Object argument) { if (!(argument instanceof TasksDeleted)) { return false; } TasksDeleted deleted = (TasksDeleted) argument; return taskIds.equals(Tasks.ids(deleted.getTasks())); } @Override public void appendTo(StringBuffer buffer) { buffer.append(taskIds); } } PubsubEvent matchTasksDeleted(String id, String... ids) { EasyMock.reportMatcher(new DeletedTasksMatcher(id, ids)); return null; } @Test public void testAddTasks() { String taskId = "a"; expect(taskIdGenerator.generate(NON_SERVICE_CONFIG, 3)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING); control.replay(); insertTask(NON_SERVICE_CONFIG, 3); ScheduledTask expected = new ScheduledTask() .setStatus(PENDING) .setTaskEvents(ImmutableList.of(new TaskEvent() .setTimestamp(clock.nowMillis()) .setScheduler(StateManagerImpl.LOCAL_HOST_SUPPLIER.get()) .setStatus(PENDING))) .setAssignedTask(new AssignedTask() .setAssignedPorts(ImmutableMap.of()) .setInstanceId(3) .setTaskId(taskId) .setTask(NON_SERVICE_CONFIG.newBuilder())); assertEquals( ImmutableSet.of(IScheduledTask.build(expected)), Storage.Util.fetchTask(storage, taskId).asSet()); } @Test public void testKillPendingTask() { String taskId = "a"; expect(taskIdGenerator.generate(NON_SERVICE_CONFIG, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING); eventSink.post(matchTasksDeleted(taskId)); control.replay(); insertTask(NON_SERVICE_CONFIG, 0); assertEquals(SUCCESS, changeState(taskId, KILLING)); assertEquals(ILLEGAL, changeState(taskId, KILLING)); } @Test public void testKillRunningTask() { String taskId = "a"; expect(taskIdGenerator.generate(NON_SERVICE_CONFIG, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING, ASSIGNED, RUNNING, KILLING, KILLED); driver.killTask(EasyMock.anyObject()); control.replay(); insertTask(NON_SERVICE_CONFIG, 0); assignTask(taskId, HOST_A); assertEquals(SUCCESS, changeState(taskId, RUNNING)); assertEquals(SUCCESS, changeState(taskId, KILLING)); assertEquals(SUCCESS, changeState(taskId, KILLED)); assertEquals(NOOP, changeState(taskId, KILLED)); } @Test public void testLostKillingTask() { String taskId = "a"; expect(taskIdGenerator.generate(NON_SERVICE_CONFIG, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING, ASSIGNED, RUNNING, KILLING, LOST); driver.killTask(EasyMock.anyObject()); control.replay(); insertTask(NON_SERVICE_CONFIG, 0); assignTask(taskId, HOST_A); changeState(taskId, RUNNING); changeState(taskId, KILLING); changeState(taskId, LOST); } @Test public void testNestedEvents() { final String id = "a"; expect(taskIdGenerator.generate(NON_SERVICE_CONFIG, 0)).andReturn(id); // Trigger an event that produces a side-effect and a PubSub event . eventSink.post(matchStateChange(id, INIT, PENDING)); expectLastCall().andAnswer(() -> { changeState(id, ASSIGNED); return null; }); // Final event sink execution that adds no side effect or event. expectStateTransitions(id, PENDING, ASSIGNED); control.replay(); insertTask(NON_SERVICE_CONFIG, 0); } @Test public void testDeletePendingTask() { String taskId = "a"; expect(taskIdGenerator.generate(NON_SERVICE_CONFIG, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING); eventSink.post(matchTasksDeleted(taskId)); control.replay(); insertTask(NON_SERVICE_CONFIG, 0); changeState(taskId, KILLING); } private static ITaskConfig setIsService(ITaskConfig config, boolean service) { return ITaskConfig.build(config.newBuilder().setIsService(service)); } @Test public void testThrottleTask() { ITaskConfig task = setIsService(NON_SERVICE_CONFIG, true); String taskId = "a"; expect(taskIdGenerator.generate(task, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING, ASSIGNED, RUNNING, FAILED); String newTaskId = "b"; expect(taskIdGenerator.generate(task, 0)).andReturn(newTaskId); expect(rescheduleCalculator.getFlappingPenaltyMs(EasyMock.anyObject())).andReturn(100L); expectStateTransitions(newTaskId, INIT, THROTTLED); control.replay(); insertTask(task, 0); changeState(taskId, ASSIGNED); changeState(taskId, RUNNING); changeState(taskId, FAILED); } @Test public void testKillUnknownTask() { String unknownTask = "unknown"; driver.killTask(unknownTask); control.replay(); changeState(unknownTask, RUNNING); } private void noFlappingPenalty() { expect(rescheduleCalculator.getFlappingPenaltyMs(EasyMock.anyObject())).andReturn(0L); } @Test public void testIncrementFailureCount() { String taskId = "a"; expect(taskIdGenerator.generate(SERVICE_CONFIG, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING, ASSIGNED, RUNNING, FAILED); String taskId2 = "a2"; expect(taskIdGenerator.generate(SERVICE_CONFIG, 0)).andReturn(taskId2); noFlappingPenalty(); expectStateTransitions(taskId2, INIT, PENDING); control.replay(); insertTask(SERVICE_CONFIG, 0); assignTask(taskId, HOST_A); changeState(taskId, RUNNING); changeState(taskId, FAILED); IScheduledTask rescheduledTask = Storage.Util.fetchTask(storage, taskId2).get(); assertEquals(taskId, rescheduledTask.getAncestorId()); assertEquals(1, rescheduledTask.getFailureCount()); } private static ITaskConfig setMaxFailures(ITaskConfig config, int maxFailures) { return ITaskConfig.build(config.newBuilder().setMaxTaskFailures(maxFailures)); } @Test public void testCasTaskPresent() { String taskId = "a"; ITaskConfig config = setMaxFailures(NON_SERVICE_CONFIG, 1); expect(taskIdGenerator.generate(config, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING, ASSIGNED, FAILED); control.replay(); insertTask(config, 0); assignTask(taskId, HOST_A); assertEquals(INVALID_CAS_STATE, changeState( taskId, Optional.of(PENDING), RUNNING, Optional.absent())); assertEquals(SUCCESS, changeState( taskId, Optional.of(ASSIGNED), FAILED, Optional.absent())); } @Test public void testCasTaskNotFound() { control.replay(); assertEquals(INVALID_CAS_STATE, changeState( "a", Optional.of(PENDING), ASSIGNED, Optional.absent())); } @Test public void testDeleteTasks() { final String taskId = "a"; expect(taskIdGenerator.generate(NON_SERVICE_CONFIG, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING, ASSIGNED, RUNNING, FINISHED); eventSink.post(matchTasksDeleted(taskId)); control.replay(); insertTask(NON_SERVICE_CONFIG, 0); assignTask(taskId, HOST_A); changeState(taskId, RUNNING); changeState(taskId, FINISHED); storage.write((NoResult.Quiet) storeProvider -> stateManager.deleteTasks(storeProvider, ImmutableSet.of(taskId))); } @Test public void testPortResource() throws Exception { Set<String> requestedPorts = ImmutableSet.of("one", "two", "three"); ITaskConfig task = resetPorts(NON_SERVICE_CONFIG, requestedPorts); String taskId = "a"; expect(taskIdGenerator.generate(task, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING, ASSIGNED); control.replay(); insertTask(task, 0); assignTask(taskId, HOST_A, ImmutableMap.of("one", 80, "two", 81, "three", 82)); IScheduledTask actual = Storage.Util.fetchTask(storage, taskId).get(); assertEquals( requestedPorts, StreamSupport.stream(ResourceManager.getTaskResources(actual, PORTS).spliterator(), false) .map(e -> e.getNamedPort()) .collect(Collectors.toSet())); } @Test public void testPortResourceResetAfterReschedule() throws Exception { Set<String> requestedPorts = ImmutableSet.of("one"); ITaskConfig task = resetPorts(NON_SERVICE_CONFIG, requestedPorts); String taskId = "a"; expect(taskIdGenerator.generate(task, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING, ASSIGNED, RUNNING, LOST); String newTaskId = "b"; expect(taskIdGenerator.generate(task, 0)).andReturn(newTaskId); expectStateTransitions(newTaskId, INIT, PENDING, ASSIGNED); noFlappingPenalty(); control.replay(); insertTask(task, 0); assignTask(taskId, HOST_A, ImmutableMap.of("one", 80)); changeState(taskId, RUNNING); changeState(taskId, LOST); assignTask(newTaskId, HOST_A, ImmutableMap.of("one", 86)); IScheduledTask actual = Storage.Util.fetchTask(storage, newTaskId).get(); assertEquals(ImmutableMap.of("one", 86), actual.getAssignedTask().getAssignedPorts()); } @Test(expected = IllegalArgumentException.class) public void insertEmptyPendingInstancesFails() { control.replay(); storage.write((NoResult.Quiet) storeProvider -> stateManager.insertPendingTasks( storeProvider, NON_SERVICE_CONFIG, ImmutableSet.of())); } @Test(expected = IllegalArgumentException.class) public void insertPendingInstancesInstanceCollision() { String taskId = "a"; expect(taskIdGenerator.generate(NON_SERVICE_CONFIG, 0)).andReturn(taskId).times(2); expectStateTransitions(taskId, INIT, PENDING); control.replay(); insertTask(NON_SERVICE_CONFIG, 0); Iterables.getOnlyElement(Storage.Util.fetchTasks(storage, Query.taskScoped(taskId))); insertTask(NON_SERVICE_CONFIG, 0); } @Test public void testAssignTaskPubsub() { // This test ensures the pubsub events emitted by assigning tasks have slave id and host set. String taskId = "a"; expect(taskIdGenerator.generate(NON_SERVICE_CONFIG, 0)).andReturn(taskId); expectStateTransitions(taskId, INIT, PENDING); Capture<TaskStateChange> taskStateChangeCapture = createCapture(); eventSink.post(capture(taskStateChangeCapture)); control.replay(); insertTask(NON_SERVICE_CONFIG, 0); assignTask(taskId, HOST_A); TaskStateChange change = taskStateChangeCapture.getValue(); assertEquals(ASSIGNED, change.getNewState()); assertEquals(HOST_A.getHost(), change.getTask().getAssignedTask().getSlaveHost()); assertEquals(HOST_A.getSlaveId(), change.getTask().getAssignedTask().getSlaveId()); } private void expectStateTransitions( String taskId, ScheduleStatus initial, ScheduleStatus next, ScheduleStatus... others) { List<ScheduleStatus> statuses = ImmutableList.<ScheduleStatus>builder() .add(initial) .add(next) .add(others) .build(); PeekingIterator<ScheduleStatus> it = Iterators.peekingIterator(statuses.iterator()); while (it.hasNext()) { ScheduleStatus cur = it.next(); try { eventSink.post(matchStateChange(taskId, cur, it.peek())); } catch (NoSuchElementException e) { // Expected. } } } private void insertTask(ITaskConfig task, int instanceId) { storage.write((NoResult.Quiet) storeProvider -> stateManager.insertPendingTasks(storeProvider, task, ImmutableSet.of(instanceId))); } private StateChangeResult changeState( String taskId, Optional<ScheduleStatus> casState, ScheduleStatus newState, Optional<String> auditMessage) { return storage.write(storeProvider -> stateManager.changeState( storeProvider, taskId, casState, newState, auditMessage)); } private StateChangeResult changeState(String taskId, ScheduleStatus status) { return changeState( taskId, Optional.absent(), status, Optional.absent()); } private void assignTask(String taskId, IHostAttributes host) { assignTask(taskId, host, ImmutableMap.of()); } private void assignTask(String taskId, IHostAttributes host, Map<String, Integer> ports) { storage.write(storeProvider -> stateManager.assignTask( storeProvider, taskId, host.getHost(), AgentID.newBuilder().setValue(host.getSlaveId()).build(), e -> IAssignedTask.build(e.newBuilder().setAssignedPorts(ports)))); } }