/**
* 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.Map;
import java.util.Set;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.apache.aurora.common.testing.easymock.EasyMockTest;
import org.apache.aurora.gen.AssignedTask;
import org.apache.aurora.gen.Attribute;
import org.apache.aurora.gen.HostAttributes;
import org.apache.aurora.gen.JobKey;
import org.apache.aurora.gen.TaskConfig;
import org.apache.aurora.scheduler.HostOffer;
import org.apache.aurora.scheduler.TierManager;
import org.apache.aurora.scheduler.base.InstanceKeys;
import org.apache.aurora.scheduler.base.TaskGroupKey;
import org.apache.aurora.scheduler.base.Tasks;
import org.apache.aurora.scheduler.filter.AttributeAggregate;
import org.apache.aurora.scheduler.filter.SchedulingFilter;
import org.apache.aurora.scheduler.filter.SchedulingFilter.ResourceRequest;
import org.apache.aurora.scheduler.filter.SchedulingFilter.UnusedResource;
import org.apache.aurora.scheduler.filter.SchedulingFilter.Veto;
import org.apache.aurora.scheduler.mesos.MesosTaskFactory;
import org.apache.aurora.scheduler.offers.OfferManager;
import org.apache.aurora.scheduler.resources.ResourceBag;
import org.apache.aurora.scheduler.state.TaskAssigner.TaskAssignerImpl;
import org.apache.aurora.scheduler.storage.entities.IAssignedTask;
import org.apache.aurora.scheduler.storage.entities.IHostAttributes;
import org.apache.aurora.scheduler.storage.entities.IInstanceKey;
import org.apache.aurora.scheduler.storage.entities.IScheduledTask;
import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
import org.apache.aurora.scheduler.testing.FakeStatsProvider;
import org.apache.aurora.scheduler.updater.UpdateAgentReserver;
import org.apache.mesos.v1.Protos.AgentID;
import org.apache.mesos.v1.Protos.FrameworkID;
import org.apache.mesos.v1.Protos.OfferID;
import org.apache.mesos.v1.Protos.Resource;
import org.apache.mesos.v1.Protos.TaskID;
import org.apache.mesos.v1.Protos.TaskInfo;
import org.apache.mesos.v1.Protos.Value.Range;
import org.apache.mesos.v1.Protos.Value.Ranges;
import org.apache.mesos.v1.Protos.Value.Type;
import org.junit.Before;
import org.junit.Test;
import static org.apache.aurora.gen.ScheduleStatus.LOST;
import static org.apache.aurora.gen.ScheduleStatus.PENDING;
import static org.apache.aurora.scheduler.base.TaskTestUtil.DEV_TIER;
import static org.apache.aurora.scheduler.base.TaskTestUtil.JOB;
import static org.apache.aurora.scheduler.base.TaskTestUtil.makeTask;
import static org.apache.aurora.scheduler.filter.AttributeAggregate.empty;
import static org.apache.aurora.scheduler.resources.ResourceManager.bagFromMesosResources;
import static org.apache.aurora.scheduler.resources.ResourceTestUtil.mesosRange;
import static org.apache.aurora.scheduler.resources.ResourceTestUtil.offer;
import static org.apache.aurora.scheduler.resources.ResourceType.PORTS;
import static org.apache.aurora.scheduler.state.TaskAssigner.TaskAssignerImpl.ASSIGNER_EVALUATED_OFFERS;
import static org.apache.aurora.scheduler.state.TaskAssigner.TaskAssignerImpl.ASSIGNER_LAUNCH_FAILURES;
import static org.apache.aurora.scheduler.state.TaskAssigner.TaskAssignerImpl.LAUNCH_FAILED_MSG;
import static org.apache.aurora.scheduler.storage.Storage.MutableStoreProvider;
import static org.apache.mesos.v1.Protos.Offer;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.anyString;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class TaskAssignerImplTest extends EasyMockTest {
private static final int PORT = 1000;
private static final Offer MESOS_OFFER = offer(mesosRange(PORTS, PORT));
private static final String SLAVE_ID = MESOS_OFFER.getAgentId().getValue();
private static final HostOffer OFFER =
new HostOffer(MESOS_OFFER, IHostAttributes.build(new HostAttributes()
.setHost(MESOS_OFFER.getHostname())
.setAttributes(ImmutableSet.of(
new Attribute("host", ImmutableSet.of(MESOS_OFFER.getHostname()))))));
private static final IScheduledTask TASK = makeTask("id", JOB);
private static final TaskGroupKey GROUP_KEY = TaskGroupKey.from(TASK.getAssignedTask().getTask());
private static final TaskInfo TASK_INFO = TaskInfo.newBuilder()
.setName("taskName")
.setTaskId(TaskID.newBuilder().setValue(Tasks.id(TASK)))
.setAgentId(MESOS_OFFER.getAgentId())
.build();
private static final IInstanceKey INSTANCE_KEY =
InstanceKeys.from(JOB, TASK.getAssignedTask().getInstanceId());
private static final Map<String, TaskGroupKey> NO_RESERVATION = ImmutableMap.of();
private static final UnusedResource UNUSED = new UnusedResource(
bagFromMesosResources(MESOS_OFFER.getResourcesList()),
OFFER.getAttributes());
private static final HostOffer OFFER_2 = new HostOffer(
Offer.newBuilder()
.setId(OfferID.newBuilder().setValue("offerId0"))
.setFrameworkId(FrameworkID.newBuilder().setValue("frameworkId"))
.setAgentId(AgentID.newBuilder().setValue("slaveId0"))
.setHostname("hostName0")
.addResources(Resource.newBuilder()
.setName("ports")
.setType(Type.RANGES)
.setRanges(
Ranges.newBuilder().addRange(Range.newBuilder().setBegin(PORT).setEnd(PORT))))
.build(),
IHostAttributes.build(new HostAttributes()));
private static final Set<String> NO_ASSIGNMENT = ImmutableSet.of();
private ResourceRequest resourceRequest;
private MutableStoreProvider storeProvider;
private StateManager stateManager;
private SchedulingFilter filter;
private MesosTaskFactory taskFactory;
private OfferManager offerManager;
private TaskAssignerImpl assigner;
private TierManager tierManager;
private FakeStatsProvider statsProvider;
private UpdateAgentReserver updateAgentReserver;
@Before
public void setUp() throws Exception {
storeProvider = createMock(MutableStoreProvider.class);
filter = createMock(SchedulingFilter.class);
taskFactory = createMock(MesosTaskFactory.class);
stateManager = createMock(StateManager.class);
offerManager = createMock(OfferManager.class);
tierManager = createMock(TierManager.class);
updateAgentReserver = createMock(UpdateAgentReserver.class);
statsProvider = new FakeStatsProvider();
assigner = new TaskAssignerImpl(
stateManager,
filter,
taskFactory,
offerManager,
tierManager,
updateAgentReserver,
statsProvider);
resourceRequest = new ResourceRequest(
TASK.getAssignedTask().getTask(),
ResourceBag.EMPTY,
empty());
}
@Test
public void testAssignNoTasks() throws Exception {
control.replay();
assertEquals(
NO_ASSIGNMENT,
assigner.maybeAssign(storeProvider, null, null, ImmutableSet.of(), null));
}
@Test
public void testAssignPartialNoVetoes() throws Exception {
expectNoUpdateReservations(1);
expect(offerManager.getOffers(GROUP_KEY)).andReturn(ImmutableSet.of(OFFER));
offerManager.launchTask(MESOS_OFFER.getId(), TASK_INFO);
expect(tierManager.getTier(TASK.getAssignedTask().getTask())).andReturn(DEV_TIER);
expect(filter.filter(UNUSED, resourceRequest)).andReturn(ImmutableSet.of());
expectAssignTask(MESOS_OFFER);
expect(taskFactory.createFrom(TASK.getAssignedTask(), MESOS_OFFER))
.andReturn(TASK_INFO);
control.replay();
AttributeAggregate aggregate = empty();
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
assertEquals(
ImmutableSet.of(Tasks.id(TASK)),
assigner.maybeAssign(
storeProvider,
new ResourceRequest(TASK.getAssignedTask().getTask(), ResourceBag.EMPTY, aggregate),
TaskGroupKey.from(TASK.getAssignedTask().getTask()),
ImmutableSet.of(
TASK.getAssignedTask(),
makeTask("id2", JOB).getAssignedTask(),
makeTask("id3", JOB).getAssignedTask()),
ImmutableMap.of(SLAVE_ID, GROUP_KEY)));
assertNotEquals(empty(), aggregate);
assertEquals(1L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
}
@Test
public void testAssignVetoesWithStaticBan() throws Exception {
expectNoUpdateReservations(1);
expect(offerManager.getOffers(GROUP_KEY)).andReturn(ImmutableSet.of(OFFER));
offerManager.banOffer(MESOS_OFFER.getId(), GROUP_KEY);
expect(tierManager.getTier(TASK.getAssignedTask().getTask())).andReturn(DEV_TIER);
expect(filter.filter(UNUSED, resourceRequest))
.andReturn(ImmutableSet.of(Veto.constraintMismatch("denied")));
control.replay();
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
assertEquals(
NO_ASSIGNMENT,
assigner.maybeAssign(
storeProvider,
resourceRequest,
TaskGroupKey.from(TASK.getAssignedTask().getTask()),
ImmutableSet.of(TASK.getAssignedTask()),
NO_RESERVATION));
assertEquals(1L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
}
@Test
public void testAssignVetoesWithNoStaticBan() throws Exception {
expectNoUpdateReservations(1);
expect(offerManager.getOffers(GROUP_KEY)).andReturn(ImmutableSet.of(OFFER));
expect(tierManager.getTier(TASK.getAssignedTask().getTask())).andReturn(DEV_TIER);
expect(filter.filter(UNUSED, resourceRequest))
.andReturn(ImmutableSet.of(Veto.unsatisfiedLimit("limit")));
control.replay();
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
assertEquals(
NO_ASSIGNMENT,
assigner.maybeAssign(
storeProvider,
resourceRequest,
TaskGroupKey.from(TASK.getAssignedTask().getTask()),
ImmutableSet.of(TASK.getAssignedTask()),
NO_RESERVATION));
assertEquals(1L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
}
@Test
public void testAssignmentClearedOnError() throws Exception {
expectNoUpdateReservations(1);
expect(offerManager.getOffers(GROUP_KEY)).andReturn(ImmutableSet.of(OFFER, OFFER_2));
offerManager.launchTask(MESOS_OFFER.getId(), TASK_INFO);
expectLastCall().andThrow(new OfferManager.LaunchException("expected"));
expect(tierManager.getTier(TASK.getAssignedTask().getTask())).andReturn(DEV_TIER);
expect(filter.filter(UNUSED, resourceRequest)).andReturn(ImmutableSet.of());
expectAssignTask(MESOS_OFFER);
expect(stateManager.changeState(
storeProvider,
Tasks.id(TASK),
Optional.of(PENDING),
LOST,
LAUNCH_FAILED_MSG))
.andReturn(StateChangeResult.SUCCESS);
expect(taskFactory.createFrom(TASK.getAssignedTask(), MESOS_OFFER))
.andReturn(TASK_INFO);
control.replay();
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_LAUNCH_FAILURES));
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
// Ensures scheduling loop terminates on the first launch failure.
assertEquals(
NO_ASSIGNMENT,
assigner.maybeAssign(
storeProvider,
resourceRequest,
TaskGroupKey.from(TASK.getAssignedTask().getTask()),
ImmutableSet.of(
TASK.getAssignedTask(),
makeTask("id2", JOB).getAssignedTask(),
makeTask("id3", JOB).getAssignedTask()),
NO_RESERVATION));
assertEquals(1L, statsProvider.getLongValue(ASSIGNER_LAUNCH_FAILURES));
assertEquals(1L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
}
@Test
public void testAssignmentSkippedForReservedSlave() throws Exception {
expectNoUpdateReservations(0);
expect(tierManager.getTier(TASK.getAssignedTask().getTask())).andReturn(DEV_TIER);
expect(offerManager.getOffers(GROUP_KEY)).andReturn(ImmutableSet.of(OFFER));
control.replay();
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
assertEquals(
NO_ASSIGNMENT,
assigner.maybeAssign(
storeProvider,
resourceRequest,
TaskGroupKey.from(TASK.getAssignedTask().getTask()),
ImmutableSet.of(TASK.getAssignedTask()),
ImmutableMap.of(SLAVE_ID, TaskGroupKey.from(
ITaskConfig.build(new TaskConfig().setJob(new JobKey("other", "e", "n")))))));
assertEquals(1L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
}
@Test
public void testTaskWithReservedSlaveLandsElsewhere() throws Exception {
// Ensures slave/task reservation relationship is only enforced in slave->task direction
// and permissive in task->slave direction. In other words, a task with a slave reservation
// should still be tried against other unreserved slaves.
expectNoUpdateReservations(1);
expect(offerManager.getOffers(GROUP_KEY)).andReturn(ImmutableSet.of(OFFER_2, OFFER));
expect(tierManager.getTier(TASK.getAssignedTask().getTask())).andReturn(DEV_TIER);
expect(filter.filter(
new UnusedResource(
bagFromMesosResources(OFFER_2.getOffer().getResourcesList()),
OFFER_2.getAttributes()),
resourceRequest)).andReturn(ImmutableSet.of());
expectAssignTask(OFFER_2.getOffer());
expect(taskFactory.createFrom(TASK.getAssignedTask(), OFFER_2.getOffer()))
.andReturn(TASK_INFO);
offerManager.launchTask(OFFER_2.getOffer().getId(), TASK_INFO);
control.replay();
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
assertEquals(
ImmutableSet.of(Tasks.id(TASK)),
assigner.maybeAssign(
storeProvider,
resourceRequest,
TaskGroupKey.from(TASK.getAssignedTask().getTask()),
ImmutableSet.of(TASK.getAssignedTask()),
ImmutableMap.of(SLAVE_ID, GROUP_KEY)));
assertEquals(1L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
}
@Test
public void testAssignerDoesNotReturnOnFirstMismatch() throws Exception {
// Ensures scheduling loop does not terminate prematurely when the first mismatch is identified.
HostOffer mismatched = new HostOffer(
Offer.newBuilder()
.setId(OfferID.newBuilder().setValue("offerId0"))
.setFrameworkId(FrameworkID.newBuilder().setValue("frameworkId"))
.setAgentId(AgentID.newBuilder().setValue("slaveId0"))
.setHostname("hostName0")
.addResources(Resource.newBuilder()
.setName("ports")
.setType(Type.RANGES)
.setRanges(
Ranges.newBuilder().addRange(Range.newBuilder().setBegin(PORT).setEnd(PORT))))
.build(),
IHostAttributes.build(new HostAttributes()));
expectNoUpdateReservations(2);
expect(offerManager.getOffers(GROUP_KEY)).andReturn(ImmutableSet.of(mismatched, OFFER));
expect(tierManager.getTier(TASK.getAssignedTask().getTask())).andReturn(DEV_TIER);
expect(filter.filter(
new UnusedResource(
bagFromMesosResources(mismatched.getOffer().getResourcesList()),
mismatched.getAttributes()),
resourceRequest))
.andReturn(ImmutableSet.of(Veto.constraintMismatch("constraint mismatch")));
offerManager.banOffer(mismatched.getOffer().getId(), GROUP_KEY);
expect(filter.filter(
new UnusedResource(
bagFromMesosResources(MESOS_OFFER.getResourcesList()), OFFER.getAttributes()),
resourceRequest))
.andReturn(ImmutableSet.of());
expectAssignTask(MESOS_OFFER);
expect(taskFactory.createFrom(TASK.getAssignedTask(), OFFER.getOffer()))
.andReturn(TASK_INFO);
offerManager.launchTask(OFFER.getOffer().getId(), TASK_INFO);
control.replay();
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
assertEquals(
ImmutableSet.of(Tasks.id(TASK)),
assigner.maybeAssign(
storeProvider,
resourceRequest,
TaskGroupKey.from(TASK.getAssignedTask().getTask()),
ImmutableSet.of(TASK.getAssignedTask()),
ImmutableMap.of(SLAVE_ID, GROUP_KEY)));
assertEquals(2L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
}
@Test
public void testResourceMapperCallback() {
AssignedTask builder = TASK.newBuilder().getAssignedTask();
builder.unsetAssignedPorts();
control.replay();
assertEquals(
TASK.getAssignedTask(),
assigner.mapAndAssignResources(MESOS_OFFER, IAssignedTask.build(builder)));
}
@Test
public void testAssignToReservedAgent() throws Exception {
expect(updateAgentReserver.hasReservations(GROUP_KEY)).andReturn(true);
expect(updateAgentReserver.getAgent(INSTANCE_KEY)).andReturn(Optional.of(SLAVE_ID));
updateAgentReserver.release(SLAVE_ID, INSTANCE_KEY);
expect(offerManager.getOffer(MESOS_OFFER.getAgentId())).andReturn(Optional.of(OFFER));
expect(filter.filter(UNUSED, resourceRequest)).andReturn(ImmutableSet.of());
expectAssignTask(MESOS_OFFER);
offerManager.launchTask(MESOS_OFFER.getId(), TASK_INFO);
expect(tierManager.getTier(TASK.getAssignedTask().getTask())).andReturn(DEV_TIER);
expect(taskFactory.createFrom(TASK.getAssignedTask(), MESOS_OFFER))
.andReturn(TASK_INFO);
control.replay();
AttributeAggregate aggregate = empty();
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
assertEquals(
ImmutableSet.of(Tasks.id(TASK)),
assigner.maybeAssign(
storeProvider,
new ResourceRequest(TASK.getAssignedTask().getTask(), ResourceBag.EMPTY, aggregate),
TaskGroupKey.from(TASK.getAssignedTask().getTask()),
ImmutableSet.of(
TASK.getAssignedTask()),
ImmutableMap.of(SLAVE_ID, GROUP_KEY)));
assertNotEquals(empty(), aggregate);
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
}
@Test
public void testAssignReservedAgentWhenOfferNotReady() throws Exception {
expect(updateAgentReserver.hasReservations(GROUP_KEY)).andReturn(true);
expect(updateAgentReserver.getAgent(INSTANCE_KEY)).andReturn(Optional.of(SLAVE_ID));
expect(offerManager.getOffer(MESOS_OFFER.getAgentId())).andReturn(Optional.of(OFFER));
expect(filter.filter(UNUSED, resourceRequest))
.andReturn(ImmutableSet.of(Veto.insufficientResources("cpu", 1)));
expect(tierManager.getTier(TASK.getAssignedTask().getTask())).andReturn(DEV_TIER);
offerManager.banOffer(MESOS_OFFER.getId(), GROUP_KEY);
expectLastCall();
control.replay();
AttributeAggregate aggregate = empty();
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
assertEquals(
ImmutableSet.of(),
assigner.maybeAssign(
storeProvider,
new ResourceRequest(TASK.getAssignedTask().getTask(), ResourceBag.EMPTY, aggregate),
TaskGroupKey.from(TASK.getAssignedTask().getTask()),
ImmutableSet.of(TASK.getAssignedTask()),
ImmutableMap.of(SLAVE_ID, GROUP_KEY)));
assertEquals(empty(), aggregate);
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
}
@Test
public void testAssignWithMixOfReservedAndNotReserved() throws Exception {
AttributeAggregate aggregate = empty();
ResourceRequest resources = new ResourceRequest(
TASK.getAssignedTask().getTask(), ResourceBag.EMPTY, aggregate);
expect(updateAgentReserver.hasReservations(GROUP_KEY)).andReturn(true);
expect(updateAgentReserver.getAgent(INSTANCE_KEY)).andReturn(Optional.of(SLAVE_ID));
updateAgentReserver.release(SLAVE_ID, INSTANCE_KEY);
expect(offerManager.getOffer(MESOS_OFFER.getAgentId())).andReturn(Optional.of(OFFER));
expect(filter.filter(UNUSED, resourceRequest)).andReturn(ImmutableSet.of());
expectAssignTask(MESOS_OFFER);
offerManager.launchTask(MESOS_OFFER.getId(), TASK_INFO);
expect(tierManager.getTier(TASK.getAssignedTask().getTask())).andReturn(DEV_TIER);
expect(taskFactory.createFrom(TASK.getAssignedTask(), MESOS_OFFER))
.andReturn(TASK_INFO);
// Normal scheduling loop for the remaining task...
expect(updateAgentReserver.getAgent(InstanceKeys.from(JOB, 9999))).andReturn(Optional.absent());
expect(offerManager.getOffers(GROUP_KEY)).andReturn(ImmutableSet.of(OFFER));
expect(updateAgentReserver.getReservations(OFFER.getOffer().getAgentId().getValue()))
.andReturn(ImmutableSet.of());
expect(filter.filter(UNUSED, resources))
.andReturn(ImmutableSet.of(Veto.constraintMismatch("lol")));
offerManager.banOffer(MESOS_OFFER.getId(), GROUP_KEY);
control.replay();
assertEquals(0L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
assertEquals(
ImmutableSet.of(Tasks.id(TASK)),
assigner.maybeAssign(
storeProvider,
resources,
TaskGroupKey.from(TASK.getAssignedTask().getTask()),
ImmutableSet.of(
TASK.getAssignedTask(),
makeTask("another-task", JOB, 9999).getAssignedTask()),
ImmutableMap.of(SLAVE_ID, GROUP_KEY)));
assertNotEquals(empty(), aggregate);
assertEquals(1L, statsProvider.getLongValue(ASSIGNER_EVALUATED_OFFERS));
}
private void expectAssignTask(Offer offer) {
expect(stateManager.assignTask(
eq(storeProvider),
eq(Tasks.id(TASK)),
eq(offer.getHostname()),
eq(offer.getAgentId()),
anyObject())).andReturn(TASK.getAssignedTask());
}
private void expectNoUpdateReservations(int offers) {
expect(updateAgentReserver.hasReservations(anyObject())).andReturn(false);
for (int i = 0; i < offers; i++) {
expect(updateAgentReserver.getReservations(anyString())).andReturn(ImmutableSet.of());
}
}
}