/** * 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.reconciliation; import java.util.concurrent.atomic.AtomicLong; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import org.apache.aurora.common.quantity.Amount; import org.apache.aurora.common.quantity.Time; import org.apache.aurora.common.stats.StatsProvider; 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.ScheduleStatus; import org.apache.aurora.gen.ScheduledTask; import org.apache.aurora.gen.TaskConfig; import org.apache.aurora.gen.TaskEvent; import org.apache.aurora.scheduler.async.DelayExecutor; import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange; import org.apache.aurora.scheduler.state.StateChangeResult; import org.apache.aurora.scheduler.state.StateManager; import org.apache.aurora.scheduler.storage.entities.IScheduledTask; import org.apache.aurora.scheduler.storage.testing.StorageTestUtil; import org.easymock.Capture; import org.easymock.EasyMock; import org.junit.Before; import org.junit.Test; import static org.apache.aurora.gen.ScheduleStatus.ASSIGNED; 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.PREEMPTING; import static org.apache.aurora.gen.ScheduleStatus.RUNNING; import static org.apache.aurora.gen.ScheduleStatus.STARTING; import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.junit.Assert.assertEquals; public class TaskTimeoutTest extends EasyMockTest { private static final String TASK_ID = "task_id"; private static final Amount<Long, Time> TIMEOUT = Amount.of(1L, Time.MINUTES); private AtomicLong timedOutTaskCounter; private DelayExecutor executor; private StorageTestUtil storageUtil; private StateManager stateManager; private FakeClock clock; private TaskTimeout timeout; private StatsProvider statsProvider; @Before public void setUp() { executor = createMock(DelayExecutor.class); storageUtil = new StorageTestUtil(this); stateManager = createMock(StateManager.class); clock = new FakeClock(); statsProvider = createMock(StatsProvider.class); timedOutTaskCounter = new AtomicLong(); expect(statsProvider.makeCounter(TaskTimeout.TIMED_OUT_TASKS_COUNTER)) .andReturn(timedOutTaskCounter); } private void replayAndCreate() { control.replay(); timeout = new TaskTimeout( executor, storageUtil.storage, stateManager, TIMEOUT, statsProvider); timeout.startAsync().awaitRunning(); } private Capture<Runnable> expectTaskWatch(Amount<Long, Time> expireIn) { Capture<Runnable> capture = createCapture(); executor.execute(EasyMock.capture(capture), eq(expireIn)); return capture; } private Capture<Runnable> expectTaskWatch() { return expectTaskWatch(TIMEOUT); } private void changeState(String taskId, ScheduleStatus from, ScheduleStatus to) { IScheduledTask task = IScheduledTask.build(new ScheduledTask() .setStatus(to) .setAssignedTask(new AssignedTask().setTaskId(taskId))); timeout.recordStateChange(TaskStateChange.transition(task, from)); } private void changeState(ScheduleStatus from, ScheduleStatus to) { changeState(TASK_ID, from, to); } @Test public void testNormalTransitions() { expectTaskWatch(); expectTaskWatch(); replayAndCreate(); changeState(INIT, PENDING); changeState(PENDING, ASSIGNED); changeState(ASSIGNED, STARTING); changeState(STARTING, RUNNING); changeState(RUNNING, KILLING); changeState(KILLING, KILLED); } @Test public void testTransientToTransient() { expectTaskWatch(); Capture<Runnable> killingTimeout = expectTaskWatch(); expect(storageUtil.storeProvider.getTaskStore()).andReturn(storageUtil.taskStore); storageUtil.expectRead(); storageUtil.expectTaskFetch(TASK_ID, makeTask(TASK_ID, ASSIGNED)); replayAndCreate(); changeState(PENDING, ASSIGNED); changeState(ASSIGNED, KILLING); killingTimeout.getValue().run(); assertEquals(0, timedOutTaskCounter.intValue()); } @Test public void testTimeout() throws Exception { Capture<Runnable> assignedTimeout = expectTaskWatch(); expect(storageUtil.storeProvider.getTaskStore()).andReturn(storageUtil.taskStore); storageUtil.expectRead(); storageUtil.expectTaskFetch(TASK_ID, makeTask(TASK_ID, ASSIGNED)); storageUtil.expectWrite(); expect(stateManager.changeState( storageUtil.mutableStoreProvider, TASK_ID, Optional.of(ASSIGNED), LOST, TaskTimeout.TIMEOUT_MESSAGE)) .andReturn(StateChangeResult.SUCCESS); replayAndCreate(); changeState(INIT, PENDING); changeState(PENDING, ASSIGNED); assignedTimeout.getValue().run(); assertEquals(1, timedOutTaskCounter.intValue()); } @Test public void testTaskDeleted() throws Exception { Capture<Runnable> assignedTimeout = expectTaskWatch(); expect(storageUtil.storeProvider.getTaskStore()).andReturn(storageUtil.taskStore); storageUtil.expectRead(); storageUtil.expectTaskFetch(TASK_ID); replayAndCreate(); changeState(INIT, PENDING); changeState(PENDING, KILLING); assignedTimeout.getValue().run(); assertEquals(0, timedOutTaskCounter.intValue()); } private static IScheduledTask makeTask(String taskId, ScheduleStatus status) { return makeTask(taskId, status, 0L); } private static IScheduledTask makeTask( String taskId, ScheduleStatus status, long stateEnteredMs) { return IScheduledTask.build(new ScheduledTask() .setStatus(status) .setTaskEvents(ImmutableList.of(new TaskEvent(stateEnteredMs, status))) .setAssignedTask(new AssignedTask() .setTaskId(taskId) .setTask(new TaskConfig()))); } @Test public void testStorageStart() { expectTaskWatch(TIMEOUT); expectTaskWatch(TIMEOUT); expectTaskWatch(TIMEOUT); replayAndCreate(); clock.setNowMillis(TIMEOUT.as(Time.MILLISECONDS) * 2); for (IScheduledTask task : ImmutableList.of( makeTask("a", ASSIGNED, 0), makeTask("b", KILLING, TIMEOUT.as(Time.MILLISECONDS)), makeTask("c", PREEMPTING, clock.nowMillis() + TIMEOUT.as(Time.MILLISECONDS)))) { timeout.recordStateChange(TaskStateChange.initialized(task)); } changeState("a", ASSIGNED, RUNNING); changeState("b", KILLING, KILLED); changeState("c", PREEMPTING, FINISHED); } @Test public void testTimeoutWhileNotStarted() throws Exception { // Since the timeout is never instructed to start, it should not attempt to transition tasks, // but it should try again later. Capture<Runnable> assignedTimeout = expectTaskWatch(); expectTaskWatch(TaskTimeout.NOT_STARTED_RETRY); control.replay(); timeout = new TaskTimeout(executor, storageUtil.storage, stateManager, TIMEOUT, statsProvider); changeState(INIT, PENDING); changeState(PENDING, ASSIGNED); assignedTimeout.getValue().run(); assertEquals(0, timedOutTaskCounter.intValue()); } }