/**
* 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.mesos;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InetAddresses;
import com.google.common.util.concurrent.MoreExecutors;
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.HostAttributes;
import org.apache.aurora.scheduler.HostOffer;
import org.apache.aurora.scheduler.TaskStatusHandler;
import org.apache.aurora.scheduler.base.Conversions;
import org.apache.aurora.scheduler.base.SchedulerException;
import org.apache.aurora.scheduler.events.EventSink;
import org.apache.aurora.scheduler.events.PubsubEvent;
import org.apache.aurora.scheduler.offers.OfferManager;
import org.apache.aurora.scheduler.state.MaintenanceController;
import org.apache.aurora.scheduler.storage.Storage;
import org.apache.aurora.scheduler.storage.entities.IHostAttributes;
import org.apache.aurora.scheduler.storage.testing.StorageTestUtil;
import org.apache.aurora.scheduler.testing.FakeStatsProvider;
import org.apache.mesos.v1.Protos;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.aurora.gen.MaintenanceMode.DRAINING;
import static org.apache.aurora.gen.MaintenanceMode.NONE;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.junit.Assert.assertEquals;
public class MesosCallbackHandlerTest extends EasyMockTest {
private static final Protos.AgentID AGENT_ID =
Protos.AgentID.newBuilder().setValue("agent-id").build();
private static final String MASTER_ID = "master-id";
private static final Protos.MasterInfo MASTER = Protos.MasterInfo.newBuilder()
.setId(MASTER_ID)
.setIp(InetAddresses.coerceToInteger(InetAddresses.forString("1.2.3.4"))) //NOPMD
.setPort(5050).build();
private static final Protos.ExecutorID EXECUTOR_ID =
Protos.ExecutorID.newBuilder().setValue("executor-id").build();
private static final String FRAMEWORK_ID = "framework-id";
private static final Protos.FrameworkID FRAMEWORK =
Protos.FrameworkID.newBuilder().setValue(FRAMEWORK_ID).build();
private static final String AGENT_HOST = "agent-hostname";
private static final Protos.OfferID OFFER_ID =
Protos.OfferID.newBuilder().setValue("offer-id").build();
private static final Protos.Offer OFFER = Protos.Offer.newBuilder()
.setFrameworkId(FRAMEWORK)
.setAgentId(AGENT_ID)
.setHostname(AGENT_HOST)
.setId(OFFER_ID)
.build();
private static final HostOffer HOST_OFFER = new HostOffer(
OFFER,
IHostAttributes.build(
new HostAttributes()
.setHost(AGENT_HOST)
.setSlaveId(AGENT_ID.getValue())
.setMode(NONE)
.setAttributes(ImmutableSet.of())));
private static final Protos.AgentID AGENT_ID_2 =
Protos.AgentID.newBuilder().setValue("agent-id2").build();
private static final String AGENT_HOST_2 = "agent2-hostname";
private static final Protos.OfferID OFFER_ID_2 =
Protos.OfferID.newBuilder().setValue("offer-id2").build();
private static final Protos.Offer OFFER_2 = Protos.Offer.newBuilder()
.setFrameworkId(FRAMEWORK)
.setAgentId(AGENT_ID_2)
.setHostname(AGENT_HOST_2)
.setId(OFFER_ID_2)
.build();
private static final HostOffer HOST_OFFER_2 = new HostOffer(
OFFER_2,
IHostAttributes.build(
new HostAttributes()
.setHost(AGENT_HOST_2)
.setSlaveId(AGENT_ID_2.getValue())
.setMode(NONE)
.setAttributes(ImmutableSet.of())));
private static final HostOffer DRAINING_HOST_OFFER = new HostOffer(
OFFER,
IHostAttributes.build(new HostAttributes()
.setHost(AGENT_HOST)
.setSlaveId(AGENT_ID.getValue())
.setMode(DRAINING)
.setAttributes(ImmutableSet.of())));
private static final Protos.TaskStatus STATUS_NO_REASON = Protos.TaskStatus.newBuilder()
.setState(Protos.TaskState.TASK_RUNNING)
.setSource(Protos.TaskStatus.Source.SOURCE_AGENT)
.setMessage("message")
.setTimestamp(1D)
.setTaskId(Protos.TaskID.newBuilder().setValue("task-id").build())
.build();
private static final Protos.TaskStatus STATUS = STATUS_NO_REASON
.toBuilder()
// Only testing data plumbing, this field with TASK_RUNNING would not normally happen,
.setReason(Protos.TaskStatus.Reason.REASON_COMMAND_EXECUTOR_FAILED)
.build();
private static final Protos.TaskStatus STATUS_RECONCILIATION = STATUS_NO_REASON
.toBuilder()
.setReason(Protos.TaskStatus.Reason.REASON_RECONCILIATION)
.build();
private static final Amount<Long, Time> DRAIN_THRESHOLD = Amount.of(2L, Time.MINUTES);
private static final Protos.InverseOffer INVERSE_OFFER = Protos.InverseOffer.newBuilder()
.setAgentId(AGENT_ID)
.setFrameworkId(FRAMEWORK)
.setId(OFFER_ID)
.setUnavailability(Protos.Unavailability.newBuilder()
.setStart(Protos.TimeInfo.newBuilder()
.setNanoseconds(300000000000L)
))
.build();
private static final Protos.Filters FILTER = Protos.Filters.newBuilder().build();
private StorageTestUtil storageUtil;
private Command shutdownCommand;
private TaskStatusHandler statusHandler;
private OfferManager offerManager;
private EventSink eventSink;
private FakeStatsProvider statsProvider;
private Driver driver;
private Logger injectedLog;
private FakeClock clock;
private MaintenanceController controller;
private MesosCallbackHandler handler;
@Before
public void setUp() {
storageUtil = new StorageTestUtil(this);
shutdownCommand = createMock(Command.class);
statusHandler = createMock(TaskStatusHandler.class);
offerManager = createMock(OfferManager.class);
eventSink = createMock(EventSink.class);
statsProvider = new FakeStatsProvider();
driver = createMock(Driver.class);
clock = new FakeClock();
controller = createMock(MaintenanceController.class);
createHandler(false);
}
private void createHandler(boolean mockLogger) {
if (mockLogger) {
injectedLog = createMock(Logger.class);
} else {
injectedLog = LoggerFactory.getLogger("MesosCallbackHandlerTestLogger");
}
handler = new MesosCallbackHandler.MesosCallbackHandlerImpl(
storageUtil.storage,
new Lifecycle(shutdownCommand), // Cannot mock lifecycle
statusHandler,
offerManager,
eventSink,
MoreExecutors.directExecutor(),
injectedLog,
statsProvider,
driver,
clock,
controller,
DRAIN_THRESHOLD);
}
@Test
public void testRegistration() {
storageUtil.expectOperations();
storageUtil.schedulerStore.saveFrameworkId(FRAMEWORK_ID);
expectLastCall();
eventSink.post(new PubsubEvent.DriverRegistered());
control.replay();
assertEquals(0L, statsProvider.getLongValue("framework_registered"));
handler.handleRegistration(FRAMEWORK, MASTER);
assertEquals(1L, statsProvider.getLongValue("framework_registered"));
}
@Test
public void testReRegistration() {
control.replay();
handler.handleReregistration(MASTER);
assertEquals(1L, statsProvider.getLongValue("scheduler_framework_reregisters"));
assertEquals(1L, statsProvider.getLongValue("framework_registered"));
}
@Test
public void testGetEmptyOfferList() {
control.replay();
handler.handleOffers(ImmutableList.of());
}
private void expectOfferAttributesSaved(HostOffer offer) {
expect(storageUtil.attributeStore.getHostAttributes(offer.getOffer().getHostname()))
.andReturn(Optional.absent());
IHostAttributes defaultMode = IHostAttributes.build(
Conversions.getAttributes(offer.getOffer()).newBuilder().setMode(NONE));
expect(storageUtil.attributeStore.saveHostAttributes(defaultMode)).andReturn(true);
}
@Test
public void testOffers() {
storageUtil.expectOperations();
expectOfferAttributesSaved(HOST_OFFER);
offerManager.addOffer(HOST_OFFER);
control.replay();
handler.handleOffers(ImmutableList.of(HOST_OFFER.getOffer()));
assertEquals(1L, statsProvider.getLongValue("scheduler_resource_offers"));
}
@Test
public void testMultipleOffers() {
storageUtil.expectOperations();
expectOfferAttributesSaved(HOST_OFFER);
expectOfferAttributesSaved(HOST_OFFER_2);
offerManager.addOffer(HOST_OFFER);
offerManager.addOffer(HOST_OFFER_2);
control.replay();
handler.handleOffers(ImmutableList.of(HOST_OFFER.getOffer(), HOST_OFFER_2.getOffer()));
assertEquals(2L, statsProvider.getLongValue("scheduler_resource_offers"));
}
@Test
public void testModePreservedWhenOfferAdded() {
storageUtil.expectOperations();
IHostAttributes draining =
IHostAttributes.build(HOST_OFFER.getAttributes().newBuilder().setMode(DRAINING));
expect(storageUtil.attributeStore.getHostAttributes(AGENT_HOST))
.andReturn(Optional.of(draining));
IHostAttributes saved = IHostAttributes.build(
Conversions.getAttributes(HOST_OFFER.getOffer()).newBuilder().setMode(DRAINING));
expect(storageUtil.attributeStore.saveHostAttributes(saved)).andReturn(true);
// If the host is in draining, then the offer manager should get an offer with that attribute
offerManager.addOffer(DRAINING_HOST_OFFER);
control.replay();
handler.handleOffers(ImmutableList.of(HOST_OFFER.getOffer()));
assertEquals(1L, statsProvider.getLongValue("scheduler_resource_offers"));
}
@Test
public void testDisconnection() {
eventSink.post(new PubsubEvent.DriverDisconnected());
control.replay();
handler.handleDisconnection();
assertEquals(1L, statsProvider.getLongValue("scheduler_framework_disconnects"));
assertEquals(0L, statsProvider.getLongValue("framework_registered"));
}
@Test
public void testRescind() {
offerManager.cancelOffer(OFFER_ID);
control.replay();
handler.handleRescind(OFFER_ID);
assertEquals(1L, statsProvider.getLongValue("offers_rescinded"));
}
@Test
public void testError() {
shutdownCommand.execute();
expectLastCall();
control.replay();
handler.handleError("Something bad happened!");
}
@Test
public void testUpdate() {
eventSink.post(new PubsubEvent.TaskStatusReceived(
STATUS.getState(),
Optional.fromNullable(STATUS.getSource()),
Optional.fromNullable(STATUS.getReason()),
Optional.of(1000000L)
));
statusHandler.statusUpdate(STATUS);
control.replay();
handler.handleUpdate(STATUS);
}
@Test
public void testUpdateNoSource() {
Protos.TaskStatus status = STATUS.toBuilder().clearSource().build();
eventSink.post(new PubsubEvent.TaskStatusReceived(
status.getState(),
Optional.absent(),
Optional.fromNullable(status.getReason()),
Optional.of(1000000L)
));
statusHandler.statusUpdate(status);
control.replay();
handler.handleUpdate(status);
}
@Test
public void testUpdateNoReason() {
Protos.TaskStatus status = STATUS.toBuilder().clearReason().build();
eventSink.post(new PubsubEvent.TaskStatusReceived(
status.getState(),
Optional.fromNullable(status.getSource()),
Optional.absent(),
Optional.of(1000000L)
));
statusHandler.statusUpdate(status);
control.replay();
handler.handleUpdate(status);
}
@Test
public void testUpdateNoMessage() {
Protos.TaskStatus status = STATUS.toBuilder().clearMessage().build();
eventSink.post(new PubsubEvent.TaskStatusReceived(
status.getState(),
Optional.fromNullable(status.getSource()),
Optional.fromNullable(status.getReason()),
Optional.of(1000000L)
));
statusHandler.statusUpdate(status);
control.replay();
handler.handleUpdate(status);
}
@Test(expected = SchedulerException.class)
public void testUpdateWithException() {
eventSink.post(new PubsubEvent.TaskStatusReceived(
STATUS.getState(),
Optional.fromNullable(STATUS.getSource()),
Optional.fromNullable(STATUS.getReason()),
Optional.of(1000000L)
));
statusHandler.statusUpdate(STATUS);
expectLastCall().andThrow(new Storage.StorageException("Storage Failure"));
control.replay();
handler.handleUpdate(STATUS);
}
@Test
public void testReconciliationUpdateLogging() {
// Mock the logger so we can test that it is logged at debug
createHandler(true);
String expectedMsg = "Received status update for task task-id in state TASK_RUNNING from "
+ "SOURCE_AGENT with REASON_RECONCILIATION: message";
injectedLog.debug(expectedMsg);
expectLastCall().once();
eventSink.post(new PubsubEvent.TaskStatusReceived(
STATUS_RECONCILIATION.getState(),
Optional.fromNullable(STATUS_RECONCILIATION.getSource()),
Optional.fromNullable(STATUS_RECONCILIATION.getReason()),
Optional.of(1000000L)
));
statusHandler.statusUpdate(STATUS_RECONCILIATION);
control.replay();
handler.handleUpdate(STATUS_RECONCILIATION);
}
@Test
public void testLostAgent() {
control.replay();
handler.handleLostAgent(AGENT_ID);
assertEquals(1L, statsProvider.getLongValue("slaves_lost"));
}
@Test
public void testLostExecutorIgnoresOkStatus() {
control.replay();
handler.handleLostExecutor(EXECUTOR_ID, AGENT_ID, 0);
assertEquals(0L, statsProvider.getLongValue("scheduler_lost_executors"));
}
@Test
public void testLostExecutor() {
control.replay();
handler.handleLostExecutor(EXECUTOR_ID, AGENT_ID, 1);
assertEquals(1L, statsProvider.getLongValue("scheduler_lost_executors"));
}
@Test
public void testMessage() {
// Framework messages should be ignored.
control.replay();
handler.handleMessage(EXECUTOR_ID, AGENT_ID);
}
@Test
public void testInverseOfferInTheFuture() {
driver.acceptInverseOffer(OFFER_ID, FILTER);
control.replay();
handler.handleInverseOffer(ImmutableList.of(INVERSE_OFFER));
assertEquals(1L, statsProvider.getLongValue("scheduler_inverse_offers"));
}
@Test
public void testInverseOfferWithinThreshold() {
clock.advance(Amount.of(4L, Time.MINUTES));
driver.acceptInverseOffer(OFFER_ID, FILTER);
controller.drainForInverseOffer(INVERSE_OFFER);
control.replay();
handler.handleInverseOffer(ImmutableList.of(INVERSE_OFFER));
assertEquals(1L, statsProvider.getLongValue("scheduler_inverse_offers"));
}
}