/**
* 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 java.util.Set;
import java.util.concurrent.Executor;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.TypeLiteral;
import org.apache.aurora.common.stats.StatsProvider;
import org.apache.aurora.common.testing.easymock.EasyMockTest;
import org.apache.aurora.common.util.Clock;
import org.apache.aurora.gen.ScheduledTask;
import org.apache.aurora.scheduler.async.AsyncModule.AsyncExecutor;
import org.apache.aurora.scheduler.base.JobKeys;
import org.apache.aurora.scheduler.base.Query;
import org.apache.aurora.scheduler.base.TaskGroupKey;
import org.apache.aurora.scheduler.base.TaskTestUtil;
import org.apache.aurora.scheduler.base.Tasks;
import org.apache.aurora.scheduler.configuration.executor.ExecutorSettings;
import org.apache.aurora.scheduler.events.EventSink;
import org.apache.aurora.scheduler.events.PubsubEvent.TaskStateChange;
import org.apache.aurora.scheduler.events.PubsubEventModule;
import org.apache.aurora.scheduler.filter.SchedulingFilter.ResourceRequest;
import org.apache.aurora.scheduler.preemptor.BiCache;
import org.apache.aurora.scheduler.preemptor.Preemptor;
import org.apache.aurora.scheduler.resources.ResourceBag;
import org.apache.aurora.scheduler.resources.ResourceManager;
import org.apache.aurora.scheduler.scheduling.TaskScheduler.TaskSchedulerImpl;
import org.apache.aurora.scheduler.state.PubsubTestUtil;
import org.apache.aurora.scheduler.state.TaskAssigner;
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.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.EasyMock;
import org.easymock.IExpectationSetters;
import org.junit.Before;
import org.junit.Test;
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.filter.AttributeAggregate.empty;
import static org.apache.aurora.scheduler.mesos.TestExecutorSettings.THERMOS_EXECUTOR;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
public class TaskSchedulerImplTest extends EasyMockTest {
private static final String TASK_ID = "a";
private static final IScheduledTask TASK_A =
TaskTestUtil.makeTask(TASK_ID, JobKeys.from("a", "a", "a"));
private static final TaskGroupKey GROUP_KEY =
TaskGroupKey.from(TASK_A.getAssignedTask().getTask());
private static final String SLAVE_ID = "HOST_A";
private static final Map<String, TaskGroupKey> NO_RESERVATION = ImmutableMap.of();
private static final ImmutableSet<String> SINGLE_TASK = ImmutableSet.of(TASK_ID);
private static final Set<String> SCHEDULED_RESULT = ImmutableSet.of(TASK_ID);
private static final Set<String> NOT_SCHEDULED_RESULT = ImmutableSet.of();
private StorageTestUtil storageUtil;
private TaskAssigner assigner;
private TaskScheduler scheduler;
private Preemptor preemptor;
private BiCache<String, TaskGroupKey> reservations;
private EventSink eventSink;
@Before
public void setUp() throws Exception {
storageUtil = new StorageTestUtil(this);
assigner = createMock(TaskAssigner.class);
preemptor = createMock(Preemptor.class);
reservations = createMock(new Clazz<BiCache<String, TaskGroupKey>>() { });
Injector injector = getInjector(storageUtil.storage);
scheduler = injector.getInstance(TaskScheduler.class);
eventSink = PubsubTestUtil.startPubsub(injector);
}
private Injector getInjector(Storage storageImpl) {
return Guice.createInjector(
new PubsubEventModule(),
new AbstractModule() {
@Override
protected void configure() {
bind(Executor.class).annotatedWith(AsyncExecutor.class)
.toInstance(MoreExecutors.sameThreadExecutor());
bind(new TypeLiteral<BiCache<String, TaskGroupKey>>() { }).toInstance(reservations);
bind(TaskScheduler.class).to(TaskSchedulerImpl.class);
bind(Preemptor.class).toInstance(preemptor);
bind(TaskAssigner.class).toInstance(assigner);
bind(Clock.class).toInstance(createMock(Clock.class));
bind(StatsProvider.class).toInstance(new FakeStatsProvider());
bind(Storage.class).toInstance(storageImpl);
bind(ExecutorSettings.class).toInstance(THERMOS_EXECUTOR);
PubsubEventModule.bindSubscriber(binder(), TaskScheduler.class);
}
});
}
private void expectTaskStillPendingQuery(IScheduledTask task) {
storageUtil.expectTaskFetch(
Query.taskScoped(Tasks.id(task)).byStatus(PENDING),
ImmutableSet.of(task));
}
private ResourceBag bag(IScheduledTask task) {
return ResourceManager.bagFromResources(task.getAssignedTask().getTask().getResources())
.add(THERMOS_EXECUTOR.getExecutorOverhead(task.getAssignedTask()
.getTask()
.getExecutorConfig()
.getName()).get());
}
private IExpectationSetters<Set<String>> expectAssigned(
IScheduledTask task,
Map<String, TaskGroupKey> reservationMap) {
return expect(assigner.maybeAssign(
storageUtil.mutableStoreProvider,
new ResourceRequest(task.getAssignedTask().getTask(), bag(task), empty()),
TaskGroupKey.from(task.getAssignedTask().getTask()),
ImmutableSet.of(task.getAssignedTask()),
reservationMap));
}
@Test
public void testSchedule() throws Exception {
storageUtil.expectOperations();
expectAsMap(NO_RESERVATION);
expectTaskStillPendingQuery(TASK_A);
expectActiveJobFetch(TASK_A);
expectAssigned(TASK_A, NO_RESERVATION).andReturn(SCHEDULED_RESULT);
control.replay();
assertEquals(
SCHEDULED_RESULT,
scheduler.schedule(storageUtil.mutableStoreProvider, SINGLE_TASK));
}
@Test
public void testScheduleNoTask() throws Exception {
storageUtil.expectOperations();
storageUtil.expectTaskFetch(
Query.taskScoped(Tasks.id(TASK_A)).byStatus(PENDING),
ImmutableSet.of());
control.replay();
assertEquals(
SCHEDULED_RESULT,
scheduler.schedule(storageUtil.mutableStoreProvider, SINGLE_TASK));
}
@Test
public void testSchedulePartial() throws Exception {
storageUtil.expectOperations();
String taskB = "b";
expectAsMap(NO_RESERVATION);
storageUtil.expectTaskFetch(
Query.taskScoped(Tasks.id(TASK_A), taskB).byStatus(PENDING),
ImmutableSet.of(TASK_A));
expectActiveJobFetch(TASK_A);
expectAssigned(TASK_A, NO_RESERVATION).andReturn(SCHEDULED_RESULT);
control.replay();
// Task b should be returned as well to be purged from its TaskGroup.
assertEquals(
ImmutableSet.of(TASK_ID, taskB),
scheduler.schedule(storageUtil.mutableStoreProvider, ImmutableSet.of(TASK_ID, taskB)));
}
@Test
public void testMultipleGroupsRejected() {
storageUtil.expectOperations();
String taskB = "b";
storageUtil.expectTaskFetch(
Query.taskScoped(Tasks.id(TASK_A), taskB).byStatus(PENDING),
ImmutableSet.of(TASK_A, TaskTestUtil.makeTask(taskB, JobKeys.from("b", "b", "b"))));
control.replay();
assertEquals(
NOT_SCHEDULED_RESULT,
scheduler.schedule(storageUtil.mutableStoreProvider, ImmutableSet.of(TASK_ID, taskB)));
}
@Test
public void testReservation() throws Exception {
storageUtil.expectOperations();
// No reservation available in preemptor
expectTaskStillPendingQuery(TASK_A);
expectActiveJobFetch(TASK_A);
expectAssigned(TASK_A, NO_RESERVATION).andReturn(NOT_SCHEDULED_RESULT);
expectAsMap(NO_RESERVATION);
expectNoReservation(TASK_A);
expectPreemptorCall(TASK_A, Optional.absent());
// Slave is reserved.
expectTaskStillPendingQuery(TASK_A);
expectActiveJobFetch(TASK_A);
expectAssigned(TASK_A, NO_RESERVATION).andReturn(NOT_SCHEDULED_RESULT);
expectAsMap(NO_RESERVATION);
expectNoReservation(TASK_A);
expectPreemptorCall(TASK_A, Optional.of(SLAVE_ID));
expectAddReservation(TASK_A, SLAVE_ID);
// Use previously created reservation.
expectTaskStillPendingQuery(TASK_A);
expectActiveJobFetch(TASK_A);
expectAsMap(ImmutableMap.of(SLAVE_ID, GROUP_KEY));
expectAssigned(TASK_A, ImmutableMap.of(SLAVE_ID, GROUP_KEY)).andReturn(SCHEDULED_RESULT);
control.replay();
assertEquals(
NOT_SCHEDULED_RESULT,
scheduler.schedule(storageUtil.mutableStoreProvider, SINGLE_TASK));
assertEquals(
NOT_SCHEDULED_RESULT,
scheduler.schedule(storageUtil.mutableStoreProvider, SINGLE_TASK));
assertEquals(
SCHEDULED_RESULT,
scheduler.schedule(storageUtil.mutableStoreProvider, SINGLE_TASK));
}
@Test
public void testReservationUnusable() throws Exception {
storageUtil.expectOperations();
expectTaskStillPendingQuery(TASK_A);
expectActiveJobFetch(TASK_A);
expectAsMap(NO_RESERVATION);
expectAssigned(TASK_A, NO_RESERVATION).andReturn(NOT_SCHEDULED_RESULT);
expectGetReservation(TASK_A, SLAVE_ID);
control.replay();
assertEquals(
NOT_SCHEDULED_RESULT,
scheduler.schedule(storageUtil.mutableStoreProvider, SINGLE_TASK));
}
@Test
public void testReservationRemoved() throws Exception {
storageUtil.expectOperations();
expectTaskStillPendingQuery(TASK_A);
expectActiveJobFetch(TASK_A);
expectAsMap(NO_RESERVATION);
expectAssigned(TASK_A, NO_RESERVATION).andReturn(NOT_SCHEDULED_RESULT);
expectGetReservation(TASK_A, SLAVE_ID);
control.replay();
assertEquals(
NOT_SCHEDULED_RESULT,
scheduler.schedule(storageUtil.mutableStoreProvider, SINGLE_TASK));
}
@Test
public void testNonPendingIgnored() throws Exception {
control.replay();
eventSink.post(TaskStateChange.transition(TASK_A, RUNNING));
}
@Test
public void testPendingDeletedHandled() throws Exception {
reservations.remove(SLAVE_ID, TaskGroupKey.from(TASK_A.getAssignedTask().getTask()));
control.replay();
ScheduledTask taskBuilder = TASK_A.newBuilder().setStatus(PENDING);
taskBuilder.getAssignedTask().setSlaveId(SLAVE_ID);
eventSink.post(TaskStateChange.transition(IScheduledTask.build(taskBuilder), PENDING));
}
@Test
public void testIgnoresThrottledTasks() throws Exception {
// Ensures that tasks in THROTTLED state are not considered part of the active job state.
Storage memStorage = DbUtil.createStorage();
Injector injector = getInjector(memStorage);
scheduler = injector.getInstance(TaskScheduler.class);
eventSink = PubsubTestUtil.startPubsub(injector);
ScheduledTask builder = TASK_A.newBuilder();
IScheduledTask taskA = IScheduledTask.build(builder.setStatus(PENDING));
builder.getAssignedTask().setTaskId("b");
IScheduledTask taskB = IScheduledTask.build(builder.setStatus(THROTTLED));
memStorage.write((NoResult.Quiet)
store -> store.getUnsafeTaskStore().saveTasks(ImmutableSet.of(taskA, taskB)));
expectAsMap(NO_RESERVATION);
expect(assigner.maybeAssign(
EasyMock.anyObject(),
eq(new ResourceRequest(taskA.getAssignedTask().getTask(), bag(taskA), empty())),
eq(TaskGroupKey.from(taskA.getAssignedTask().getTask())),
eq(ImmutableSet.of(taskA.getAssignedTask())),
eq(NO_RESERVATION))).andReturn(SCHEDULED_RESULT);
control.replay();
memStorage.write((NoResult.Quiet)
store -> assertEquals(SCHEDULED_RESULT, scheduler.schedule(store, SINGLE_TASK)));
}
@Test
public void testScheduleThrows() throws Exception {
storageUtil.expectOperations();
expectAsMap(NO_RESERVATION);
expectTaskStillPendingQuery(TASK_A);
expectActiveJobFetch(TASK_A);
expectAssigned(TASK_A, NO_RESERVATION).andThrow(new IllegalArgumentException("expected"));
control.replay();
assertEquals(
NOT_SCHEDULED_RESULT,
scheduler.schedule(storageUtil.mutableStoreProvider, SINGLE_TASK));
}
private void expectPreemptorCall(IScheduledTask task, Optional<String> result) {
expect(preemptor.attemptPreemptionFor(
task.getAssignedTask(),
empty(),
storageUtil.mutableStoreProvider)).andReturn(result);
}
private void expectActiveJobFetch(IScheduledTask task) {
storageUtil.expectTaskFetch(
Query.jobScoped(((Function<IScheduledTask, IJobKey>) Tasks::getJob).apply(task))
.byStatus(Tasks.SLAVE_ASSIGNED_STATES),
ImmutableSet.of());
}
private void expectAddReservation(IScheduledTask task, String slaveId) {
reservations.put(slaveId, TaskGroupKey.from(task.getAssignedTask().getTask()));
}
private IExpectationSetters<?> expectGetReservation(IScheduledTask task, String slaveId) {
return expect(reservations.getByValue(TaskGroupKey.from(task.getAssignedTask().getTask())))
.andReturn(ImmutableSet.of(slaveId));
}
private IExpectationSetters<?> expectNoReservation(IScheduledTask task) {
return expect(reservations.getByValue(TaskGroupKey.from(task.getAssignedTask().getTask())))
.andReturn(ImmutableSet.of());
}
private IExpectationSetters<?> expectAsMap(Map<String, TaskGroupKey> map) {
return expect(reservations.asMap()).andReturn(map);
}
}