/**
* 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.scheduling;
import java.util.Map;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import org.apache.aurora.common.quantity.Amount;
import org.apache.aurora.common.quantity.Time;
import org.apache.aurora.common.testing.easymock.EasyMockTest;
import org.apache.aurora.common.util.BackoffStrategy;
import org.apache.aurora.gen.ScheduleStatus;
import org.apache.aurora.gen.ScheduledTask;
import org.apache.aurora.gen.TaskEvent;
import org.apache.aurora.scheduler.base.TaskTestUtil;
import org.apache.aurora.scheduler.base.Tasks;
import org.apache.aurora.scheduler.scheduling.RescheduleCalculator.RescheduleCalculatorImpl;
import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
import org.apache.aurora.scheduler.storage.testing.StorageTestUtil;
import org.junit.Before;
import org.junit.Test;
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.PENDING;
import static org.apache.aurora.gen.ScheduleStatus.RUNNING;
import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
public class RescheduleCalculatorImplTest extends EasyMockTest {
private static final Amount<Long, Time> FLAPPING_THRESHOLD = Amount.of(1L, Time.MINUTES);
private static final Amount<Integer, Time> MAX_STARTUP_DELAY = Amount.of(10, Time.MINUTES);
private StorageTestUtil storageUtil;
private BackoffStrategy backoff;
private RescheduleCalculator rescheduleCalculator;
@Before
public void setUp() {
storageUtil = new StorageTestUtil(this);
backoff = createMock(BackoffStrategy.class);
rescheduleCalculator = new RescheduleCalculatorImpl(
storageUtil.storage,
new RescheduleCalculatorImpl.RescheduleCalculatorSettings(
backoff,
FLAPPING_THRESHOLD,
MAX_STARTUP_DELAY));
storageUtil.expectOperations();
}
@Test
public void testNoPenaltyForNoAncestor() {
control.replay();
assertEquals(0L, rescheduleCalculator.getFlappingPenaltyMs(makeTask("a", INIT)));
}
@Test
public void testNoPenaltyDeletedAncestor() {
String ancestorId = "a";
storageUtil.expectTaskFetch(ancestorId);
control.replay();
assertEquals(
0L,
rescheduleCalculator.getFlappingPenaltyMs(setAncestor(makeTask("b", INIT), ancestorId)));
}
@Test
public void testFlappingTask() {
IScheduledTask ancestor = makeFlappyTask("a");
storageUtil.expectTaskFetch(Tasks.id(ancestor), ancestor);
long penaltyMs = 1000L;
expect(backoff.calculateBackoffMs(0L)).andReturn(penaltyMs);
control.replay();
assertEquals(
penaltyMs,
rescheduleCalculator.getFlappingPenaltyMs(
setAncestor(makeTask("b", INIT), Tasks.id(ancestor))));
}
@Test
public void testFlappingTasksBackoffTruncation() {
// Ensures that the reschedule calculator detects penalty truncation and avoids inspecting
// ancestors once truncated.
IScheduledTask taskA = setAncestor(makeFlappyTask("a"), "bugIfQueried");
IScheduledTask taskB = setAncestor(makeFlappyTask("b"), Tasks.id(taskA));
IScheduledTask taskC = setAncestor(makeFlappyTask("c"), Tasks.id(taskB));
IScheduledTask taskD = setAncestor(makeFlappyTask("d"), Tasks.id(taskC));
Map<IScheduledTask, Long> ancestorsAndPenalties = ImmutableMap.of(
taskD, 100L,
taskC, 200L,
taskB, 300L,
taskA, 300L);
long lastPenalty = 0L;
for (Map.Entry<IScheduledTask, Long> taskAndPenalty : ancestorsAndPenalties.entrySet()) {
storageUtil.expectTaskFetch(Tasks.id(taskAndPenalty.getKey()), taskAndPenalty.getKey());
expect(backoff.calculateBackoffMs(lastPenalty)).andReturn(taskAndPenalty.getValue());
lastPenalty = taskAndPenalty.getValue();
}
control.replay();
IScheduledTask newTask = setAncestor(makeFlappyTask("newTask"), Tasks.id(taskD));
assertEquals(300L, rescheduleCalculator.getFlappingPenaltyMs(newTask));
}
@Test
public void testNoPenaltyForInterruptedTasks() {
IScheduledTask ancestor = setEvents(
makeTask("a", KILLED),
ImmutableMap.of(INIT, 0L, PENDING, 100L, RUNNING, 200L, KILLING, 300L, KILLED, 400L));
storageUtil.expectTaskFetch(Tasks.id(ancestor), ancestor);
control.replay();
assertEquals(
0L,
rescheduleCalculator.getFlappingPenaltyMs(
setAncestor(makeTask("b", INIT), Tasks.id(ancestor))));
}
private IScheduledTask makeFlappyTask(String taskId) {
return setEvents(
makeTask(taskId, FINISHED),
ImmutableMap.of(INIT, 0L, PENDING, 100L, RUNNING, 200L, FINISHED, 300L));
}
private IScheduledTask makeTask(String taskId) {
ScheduledTask builder = TaskTestUtil.makeTask(taskId, TaskTestUtil.JOB).newBuilder();
builder.unsetAncestorId();
return IScheduledTask.build(builder);
}
private IScheduledTask makeTask(String taskId, ScheduleStatus status) {
return IScheduledTask.build(makeTask(taskId).newBuilder().setStatus(status));
}
private IScheduledTask setAncestor(IScheduledTask task, String ancestorId) {
return IScheduledTask.build(task.newBuilder().setAncestorId(ancestorId));
}
private static final Function<Map.Entry<ScheduleStatus, Long>, TaskEvent> TO_EVENT =
input -> new TaskEvent().setStatus(input.getKey()).setTimestamp(input.getValue());
private IScheduledTask setEvents(IScheduledTask task, Map<ScheduleStatus, Long> events) {
return IScheduledTask.build(task.newBuilder().setTaskEvents(
FluentIterable.from(events.entrySet()).transform(TO_EVENT).toList()));
}
}