/** * 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.Set; import java.util.concurrent.Executor; import com.google.common.base.Optional; 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 org.apache.aurora.common.quantity.Amount; import org.apache.aurora.common.quantity.Time; import org.apache.aurora.common.stats.StatsProvider; import org.apache.aurora.common.testing.easymock.EasyMockTest; import org.apache.aurora.gen.HostAttributes; import org.apache.aurora.gen.HostStatus; import org.apache.aurora.gen.MaintenanceMode; 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.AsyncModule.AsyncExecutor; import org.apache.aurora.scheduler.base.Query; import org.apache.aurora.scheduler.base.TaskTestUtil; import org.apache.aurora.scheduler.base.Tasks; 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.storage.Storage; import org.apache.aurora.scheduler.storage.entities.IHostAttributes; import org.apache.aurora.scheduler.storage.entities.IHostStatus; 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.apache.mesos.v1.Protos; import org.junit.Before; import org.junit.Test; import static org.apache.aurora.gen.MaintenanceMode.DRAINED; import static org.apache.aurora.gen.MaintenanceMode.DRAINING; import static org.apache.aurora.gen.MaintenanceMode.NONE; import static org.apache.aurora.gen.MaintenanceMode.SCHEDULED; import static org.apache.aurora.gen.ScheduleStatus.KILLED; import static org.apache.aurora.gen.ScheduleStatus.RUNNING; import static org.apache.aurora.scheduler.state.MaintenanceController.MaintenanceControllerImpl; import static org.apache.aurora.scheduler.testing.BatchWorkerUtil.expectBatchExecute; import static org.easymock.EasyMock.expect; import static org.junit.Assert.assertEquals; public class MaintenanceControllerImplTest extends EasyMockTest { private static final String HOST_A = "a"; private static final Set<String> A = ImmutableSet.of(HOST_A); private static final Protos.OfferID OFFER_ID = Protos.OfferID.newBuilder() .setValue("offer-id") .build(); private static final Protos.AgentID AGENT_ID = Protos.AgentID.newBuilder() .setValue("agent-id") .build(); private static final Protos.FrameworkID FRAMEWORK_ID = Protos.FrameworkID.newBuilder() .setValue("framework-id") .build(); private static final Protos.URL AGENT_URL = Protos.URL.newBuilder() .setAddress(Protos.Address.newBuilder() .setHostname(HOST_A) .setPort(5051)) .setScheme("http") .build(); private static final Protos.Unavailability UNAVAILABILITY = Protos.Unavailability.newBuilder() .setStart(Protos.TimeInfo.newBuilder() .setNanoseconds(Amount.of(1L, Time.MINUTES).as(Time.NANOSECONDS))) .build(); private static final Protos.InverseOffer INVERSE_OFFER = Protos.InverseOffer.newBuilder() .setId(OFFER_ID) .setAgentId(AGENT_ID) .setUrl(AGENT_URL) .setFrameworkId(FRAMEWORK_ID) .setUnavailability(UNAVAILABILITY) .build(); private StorageTestUtil storageUtil; private StateManager stateManager; private MaintenanceController maintenance; private EventSink eventSink; @Before public void setUp() throws Exception { storageUtil = new StorageTestUtil(this); storageUtil.expectOperations(); stateManager = createMock(StateManager.class); TaskEventBatchWorker batchWorker = createMock(TaskEventBatchWorker.class); expectBatchExecute(batchWorker, storageUtil.storage, control).anyTimes(); Injector injector = Guice.createInjector( new PubsubEventModule(), new AbstractModule() { @Override protected void configure() { StateModule.bindMaintenanceController(binder()); bind(Storage.class).toInstance(storageUtil.storage); bind(StateManager.class).toInstance(stateManager); bind(StatsProvider.class).toInstance(new FakeStatsProvider()); bind(Executor.class).annotatedWith(AsyncExecutor.class) .toInstance(MoreExecutors.directExecutor()); bind(TaskEventBatchWorker.class).toInstance(batchWorker); } }); maintenance = injector.getInstance(MaintenanceController.class); eventSink = PubsubTestUtil.startPubsub(injector); } private static IScheduledTask makeTask(String host, String taskId) { ScheduledTask builder = TaskTestUtil.addStateTransition( TaskTestUtil.makeTask(taskId, TaskTestUtil.JOB), RUNNING, 1000).newBuilder(); builder.getAssignedTask().setSlaveHost(host); return IScheduledTask.build(builder); } @Test public void testMaintenanceCycle() { IScheduledTask task1 = makeTask(HOST_A, "taskA"); IScheduledTask task2 = makeTask(HOST_A, "taskB"); expectMaintenanceModeChange(HOST_A, SCHEDULED); expectFetchTasksByHost(HOST_A, ImmutableSet.of(task1, task2)); expectTaskDraining(task1); expectTaskDraining(task2); expectMaintenanceModeChange(HOST_A, DRAINING); IHostAttributes attributes = IHostAttributes.build(new HostAttributes().setHost(HOST_A).setMode(DRAINING)); expect(storageUtil.attributeStore.getHostAttributes(HOST_A)) .andReturn(Optional.of(attributes)).times(2); expect(storageUtil.attributeStore.getHostAttributes()).andReturn(ImmutableSet.of(attributes)); expectFetchTasksByHost(HOST_A, ImmutableSet.of(task2)); // TaskA is KILLED and therefore no longer active expectFetchTasksByHost(HOST_A, ImmutableSet.of()); expectMaintenanceModeChange(HOST_A, DRAINED); expectMaintenanceModeChange(HOST_A, NONE); control.replay(); assertStatus(HOST_A, SCHEDULED, maintenance.startMaintenance(A)); assertStatus(HOST_A, DRAINING, maintenance.drain(A)); assertStatus(HOST_A, DRAINING, maintenance.getStatus(A)); eventSink.post( TaskStateChange.transition( IScheduledTask.build(task1.newBuilder().setStatus(KILLED)), RUNNING)); eventSink.post( TaskStateChange.transition( IScheduledTask.build(task2.newBuilder().setStatus(KILLED)), RUNNING)); assertStatus(HOST_A, NONE, maintenance.endMaintenance(A)); } @Test public void testUnknownHost() { expect(storageUtil.attributeStore.getHostAttributes("b")) .andReturn(Optional.absent()); control.replay(); assertEquals(ImmutableSet.of(), maintenance.startMaintenance(ImmutableSet.of("b"))); } @Test public void testDrainEmptyHost() { expectMaintenanceModeChange(HOST_A, SCHEDULED); expectFetchTasksByHost(HOST_A, ImmutableSet.of()); expectMaintenanceModeChange(HOST_A, DRAINED); control.replay(); assertStatus(HOST_A, SCHEDULED, maintenance.startMaintenance(A)); assertStatus(HOST_A, DRAINED, maintenance.drain(A)); } @Test public void testEndEarly() { expectMaintenanceModeChange(HOST_A, SCHEDULED); expectMaintenanceModeChange(HOST_A, NONE); expect(storageUtil.attributeStore.getHostAttributes(HOST_A)).andReturn(Optional.of( IHostAttributes.build(new HostAttributes().setHost(HOST_A).setMode(NONE)))); control.replay(); assertStatus(HOST_A, SCHEDULED, maintenance.startMaintenance(A)); // End maintenance without DRAINING. assertStatus(HOST_A, NONE, maintenance.endMaintenance(A)); // Make sure a later transition on the host does not cause any ill effects that could surface // from stale internal state. eventSink.post(TaskStateChange.transition( IScheduledTask.build(makeTask(HOST_A, "taskA").newBuilder().setStatus(KILLED)), RUNNING)); } @Test public void testGetMode() { expect(storageUtil.attributeStore.getHostAttributes(HOST_A)).andReturn(Optional.of( IHostAttributes.build(new HostAttributes().setHost(HOST_A).setMode(DRAINING)))); expect(storageUtil.attributeStore.getHostAttributes("unknown")).andReturn(Optional.absent()); control.replay(); assertEquals(DRAINING, maintenance.getMode(HOST_A)); assertEquals(NONE, maintenance.getMode("unknown")); } @Test public void testInverseOfferDrain() { IScheduledTask task1 = makeTask(HOST_A, "taskA"); expectFetchTasksByHost(HOST_A, ImmutableSet.of(task1)); expectTaskDraining(task1); control.replay(); maintenance.drainForInverseOffer(INVERSE_OFFER); } private void expectTaskDraining(IScheduledTask task) { expect(stateManager.changeState( storageUtil.mutableStoreProvider, Tasks.id(task), Optional.absent(), ScheduleStatus.DRAINING, MaintenanceControllerImpl.DRAINING_MESSAGE)) .andReturn(StateChangeResult.SUCCESS); } private void expectFetchTasksByHost(String hostName, Set<IScheduledTask> tasks) { expect(storageUtil.taskStore.fetchTasks(Query.slaveScoped(hostName).active())).andReturn(tasks); } private void expectMaintenanceModeChange(String hostName, MaintenanceMode mode) { IHostAttributes attributes = IHostAttributes.build(new HostAttributes().setHost(hostName)); expect(storageUtil.attributeStore.getHostAttributes(hostName)) .andReturn(Optional.of(attributes)); IHostAttributes updated = IHostAttributes.build(attributes.newBuilder().setMode(mode)); expect(storageUtil.attributeStore.saveHostAttributes(updated)).andReturn(true); } private void assertStatus(String host, MaintenanceMode mode, Set<IHostStatus> statuses) { assertEquals(ImmutableSet.of(IHostStatus.build(new HostStatus(host, mode))), statuses); } }