/**
* 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.pruning;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.io.Closer;
import org.apache.aurora.common.application.Lifecycle;
import org.apache.aurora.common.base.Command;
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.testing.FakeClock;
import org.apache.aurora.gen.ScheduleStatus;
import org.apache.aurora.gen.ScheduledTask;
import org.apache.aurora.scheduler.SchedulerModule.TaskEventBatchWorker;
import org.apache.aurora.scheduler.async.DelayExecutor;
import org.apache.aurora.scheduler.base.JobKeys;
import org.apache.aurora.scheduler.base.TaskTestUtil;
import org.apache.aurora.scheduler.base.Tasks;
import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange;
import org.apache.aurora.scheduler.pruning.TaskHistoryPruner.HistoryPrunnerSettings;
import org.apache.aurora.scheduler.state.StateManager;
import org.apache.aurora.scheduler.storage.entities.IJobKey;
import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
import org.apache.aurora.scheduler.storage.testing.StorageTestUtil;
import org.apache.aurora.scheduler.testing.FakeStatsProvider;
import org.easymock.Capture;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.apache.aurora.gen.ScheduleStatus.FINISHED;
import static org.apache.aurora.gen.ScheduleStatus.KILLED;
import static org.apache.aurora.gen.ScheduleStatus.LOST;
import static org.apache.aurora.gen.ScheduleStatus.RUNNING;
import static org.apache.aurora.gen.ScheduleStatus.STARTING;
import static org.apache.aurora.scheduler.pruning.TaskHistoryPruner.TASKS_PRUNED;
import static org.apache.aurora.scheduler.testing.BatchWorkerUtil.expectBatchExecute;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expectLastCall;
import static org.junit.Assert.assertEquals;
public class TaskHistoryPrunerTest extends EasyMockTest {
private static final String SLAVE_HOST = "HOST_A";
private static final Amount<Long, Time> ONE_MS = Amount.of(1L, Time.MILLISECONDS);
private static final Amount<Long, Time> ONE_MINUTE = Amount.of(1L, Time.MINUTES);
private static final Amount<Long, Time> ONE_DAY = Amount.of(1L, Time.DAYS);
private static final Amount<Long, Time> ONE_HOUR = Amount.of(1L, Time.HOURS);
private static final int PER_JOB_HISTORY = 2;
private DelayExecutor executor;
private FakeClock clock;
private StateManager stateManager;
private StorageTestUtil storageUtil;
private TaskHistoryPruner pruner;
private Closer closer;
private Command shutdownCommand;
private FakeStatsProvider statsProvider;
@Before
public void setUp() throws Exception {
executor = createMock(DelayExecutor.class);
clock = new FakeClock();
stateManager = createMock(StateManager.class);
storageUtil = new StorageTestUtil(this);
storageUtil.expectOperations();
shutdownCommand = createMock(Command.class);
TaskEventBatchWorker batchWorker = createMock(TaskEventBatchWorker.class);
statsProvider = new FakeStatsProvider();
expectBatchExecute(batchWorker, storageUtil.storage, control).anyTimes();
pruner = new TaskHistoryPruner(
executor,
stateManager,
clock,
new HistoryPrunnerSettings(ONE_DAY, ONE_MINUTE, PER_JOB_HISTORY),
storageUtil.storage,
new Lifecycle(shutdownCommand),
batchWorker,
statsProvider);
closer = Closer.create();
}
@After
public void tearDownCloser() throws Exception {
closer.close();
}
@Test
public void testNoPruning() {
long taskATimestamp = clock.nowMillis();
IScheduledTask a = makeTask("a", FINISHED);
clock.advance(ONE_MS);
long taskBTimestamp = clock.nowMillis();
IScheduledTask b = makeTask("b", LOST);
expectNoImmediatePrune(ImmutableSet.of(a));
expectOneDelayedPrune(taskATimestamp);
expectNoImmediatePrune(ImmutableSet.of(a, b));
expectOneDelayedPrune(taskBTimestamp);
control.replay();
pruner.recordStateChange(TaskStateChange.initialized(a));
pruner.recordStateChange(TaskStateChange.initialized(b));
}
@Test
public void testStorageStartedWithPruning() {
long taskATimestamp = clock.nowMillis();
IScheduledTask a = makeTask("a", FINISHED);
clock.advance(ONE_MINUTE);
long taskBTimestamp = clock.nowMillis();
IScheduledTask b = makeTask("b", LOST);
clock.advance(ONE_MINUTE);
long taskCTimestamp = clock.nowMillis();
IScheduledTask c = makeTask("c", FINISHED);
clock.advance(ONE_MINUTE);
IScheduledTask d = makeTask("d", FINISHED);
IScheduledTask e = makeTask(JobKeys.from("role", "env", "job-x"), "e", FINISHED);
expectNoImmediatePrune(ImmutableSet.of(a));
expectOneDelayedPrune(taskATimestamp);
expectNoImmediatePrune(ImmutableSet.of(a, b));
expectOneDelayedPrune(taskBTimestamp);
expectImmediatePrune(ImmutableSet.of(a, b, c), a);
expectOneDelayedPrune(taskCTimestamp);
expectImmediatePrune(ImmutableSet.of(b, c, d), b);
expectDefaultDelayedPrune();
expectNoImmediatePrune(ImmutableSet.of(e));
expectDefaultDelayedPrune();
control.replay();
assertEquals(0L, statsProvider.getValue(TASKS_PRUNED));
for (IScheduledTask task : ImmutableList.of(a, b, c, d, e)) {
pruner.recordStateChange(TaskStateChange.initialized(task));
}
assertEquals(2L, statsProvider.getValue(TASKS_PRUNED));
}
@Test
public void testStateChange() {
IScheduledTask starting = makeTask("a", STARTING);
IScheduledTask running = copy(starting, RUNNING);
IScheduledTask killed = copy(starting, KILLED);
expectNoImmediatePrune(ImmutableSet.of(killed));
expectDefaultDelayedPrune();
control.replay();
// No future set for non-terminal state transition.
changeState(starting, running);
// Future set for terminal state transition.
changeState(running, killed);
}
@Test
public void testActivateFutureAndExceedHistoryGoal() {
IScheduledTask running = makeTask("a", RUNNING);
IScheduledTask killed = copy(running, KILLED);
expectNoImmediatePrune(ImmutableSet.of(running));
Capture<Runnable> delayedDelete = expectDefaultDelayedPrune();
// Expect task "a" to be pruned when future is activated.
expectDeleteTasks("a");
control.replay();
// Capture future for inactive task "a"
changeState(running, killed);
clock.advance(ONE_HOUR);
assertEquals(0L, statsProvider.getValue(TASKS_PRUNED));
// Execute future to prune task "a" from the system.
delayedDelete.getValue().run();
assertEquals(1L, statsProvider.getValue(TASKS_PRUNED));
}
@Test
public void testSuppressEmptyDelete() {
IScheduledTask running = makeTask("a", RUNNING);
IScheduledTask killed = copy(running, KILLED);
expectImmediatePrune(
ImmutableSet.of(makeTask("b", KILLED), makeTask("c", KILLED), makeTask("d", KILLED)));
expectDefaultDelayedPrune();
control.replay();
changeState(running, killed);
}
@Test
public void testJobHistoryExceeded() {
IScheduledTask a = makeTask("a", RUNNING);
clock.advance(ONE_MS);
IScheduledTask aKilled = copy(a, KILLED);
IScheduledTask b = makeTask("b", RUNNING);
clock.advance(ONE_MS);
IScheduledTask bKilled = copy(b, KILLED);
IScheduledTask c = makeTask("c", RUNNING);
clock.advance(ONE_MS);
IScheduledTask cLost = copy(c, LOST);
IScheduledTask d = makeTask("d", RUNNING);
clock.advance(ONE_MS);
IScheduledTask dLost = copy(d, LOST);
expectNoImmediatePrune(ImmutableSet.of(a));
expectDefaultDelayedPrune();
expectNoImmediatePrune(ImmutableSet.of(a, b));
expectDefaultDelayedPrune();
expectNoImmediatePrune(ImmutableSet.of(a, b)); // no pruning yet due to min threshold
expectDefaultDelayedPrune();
clock.advance(ONE_HOUR);
expectImmediatePrune(ImmutableSet.of(a, b, c, d), a, b); // now prune 2 tasks
expectDefaultDelayedPrune();
control.replay();
changeState(a, aKilled);
changeState(b, bKilled);
changeState(c, cLost);
assertEquals(0L, statsProvider.getValue(TASKS_PRUNED));
changeState(d, dLost);
assertEquals(2L, statsProvider.getValue(TASKS_PRUNED));
}
@Test
public void serviceShutdownOnFailure() {
IScheduledTask running = makeTask("a", RUNNING);
IScheduledTask killed = copy(running, KILLED);
expectNoImmediatePrune(ImmutableSet.of(running));
Capture<Runnable> delayedDelete = expectDefaultDelayedPrune();
expectDeleteTasks("a");
expectLastCall().andThrow(new RuntimeException("oops"));
shutdownCommand.execute();
control.replay();
changeState(running, killed);
clock.advance(ONE_HOUR);
assertEquals(0L, statsProvider.getValue(TASKS_PRUNED));
delayedDelete.getValue().run();
assertEquals(0L, statsProvider.getValue(TASKS_PRUNED));
}
private void expectDeleteTasks(String... tasks) {
stateManager.deleteTasks(storageUtil.mutableStoreProvider, ImmutableSet.copyOf(tasks));
}
private Capture<Runnable> expectDefaultDelayedPrune() {
return expectDelayedPrune(ONE_DAY.as(Time.MILLISECONDS));
}
private Capture<Runnable> expectOneDelayedPrune(long timestampMillis) {
return expectDelayedPrune(timestampMillis);
}
private void expectNoImmediatePrune(ImmutableSet<IScheduledTask> tasksInJob) {
expectImmediatePrune(tasksInJob);
}
private void expectImmediatePrune(
ImmutableSet<IScheduledTask> tasksInJob,
IScheduledTask... pruned) {
// Expect a deferred prune operation when a new task is being watched.
executor.execute(EasyMock.<Runnable>anyObject());
expectLastCall().andAnswer(
() -> {
Runnable work = (Runnable) EasyMock.getCurrentArguments()[0];
work.run();
return null;
}
);
IJobKey jobKey = Iterables.getOnlyElement(
FluentIterable.from(tasksInJob).transform(Tasks::getJob).toSet());
storageUtil.expectTaskFetch(TaskHistoryPruner.jobHistoryQuery(jobKey), tasksInJob);
if (pruned.length > 0) {
stateManager.deleteTasks(storageUtil.mutableStoreProvider, Tasks.ids(pruned));
}
}
private Capture<Runnable> expectDelayedPrune(long timestampMillis) {
Capture<Runnable> capture = createCapture();
executor.execute(
EasyMock.capture(capture),
eq(Amount.of(pruner.calculateTimeout(timestampMillis), Time.MILLISECONDS)));
return capture;
}
private void changeState(IScheduledTask oldStateTask, IScheduledTask newStateTask) {
pruner.recordStateChange(TaskStateChange.transition(newStateTask, oldStateTask.getStatus()));
}
private IScheduledTask copy(IScheduledTask task, ScheduleStatus status) {
return IScheduledTask.build(task.newBuilder().setStatus(status));
}
private IScheduledTask makeTask(
IJobKey job,
String taskId,
ScheduleStatus status) {
ScheduledTask builder = TaskTestUtil.addStateTransition(
TaskTestUtil.makeTask(taskId, job), status, clock.nowMillis())
.newBuilder();
builder.getAssignedTask().setSlaveHost(SLAVE_HOST);
return IScheduledTask.build(builder);
}
private IScheduledTask makeTask(String taskId, ScheduleStatus status) {
return makeTask(TaskTestUtil.JOB, taskId, status);
}
}