/** * 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.offers; import java.util.List; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; 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.gen.HostAttributes; import org.apache.aurora.gen.MaintenanceMode; import org.apache.aurora.scheduler.HostOffer; import org.apache.aurora.scheduler.async.DelayExecutor; import org.apache.aurora.scheduler.base.TaskGroupKey; import org.apache.aurora.scheduler.base.Tasks; import org.apache.aurora.scheduler.events.PubsubEvent.DriverDisconnected; import org.apache.aurora.scheduler.events.PubsubEvent.HostAttributesChanged; import org.apache.aurora.scheduler.mesos.Driver; import org.apache.aurora.scheduler.offers.OfferManager.OfferManagerImpl; import org.apache.aurora.scheduler.storage.entities.IHostAttributes; import org.apache.aurora.scheduler.storage.entities.IScheduledTask; import org.apache.aurora.scheduler.testing.FakeScheduledExecutor; import org.apache.aurora.scheduler.testing.FakeStatsProvider; import org.apache.mesos.v1.Protos; import org.apache.mesos.v1.Protos.Filters; import org.apache.mesos.v1.Protos.Offer.Operation; import org.apache.mesos.v1.Protos.TaskInfo; import org.apache.mesos.v1.Protos.TimeInfo; import org.apache.mesos.v1.Protos.Unavailability; import org.junit.Before; import org.junit.Test; import static org.apache.aurora.gen.MaintenanceMode.DRAINING; import static org.apache.aurora.gen.MaintenanceMode.NONE; import static org.apache.aurora.scheduler.base.TaskTestUtil.JOB; import static org.apache.aurora.scheduler.base.TaskTestUtil.makeTask; import static org.apache.aurora.scheduler.offers.OfferManager.OfferManagerImpl.OFFER_ACCEPT_RACES; import static org.apache.aurora.scheduler.offers.OfferManager.OfferManagerImpl.OUTSTANDING_OFFERS; import static org.apache.aurora.scheduler.offers.OfferManager.OfferManagerImpl.STATICALLY_BANNED_OFFERS; 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.easymock.EasyMock.expectLastCall; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class OfferManagerImplTest extends EasyMockTest { private static final Amount<Long, Time> RETURN_DELAY = Amount.of(1L, Time.DAYS); private static final Amount<Long, Time> ONE_HOUR = Amount.of(1L, Time.HOURS); private static final String HOST_A = "HOST_A"; private static final IHostAttributes HOST_ATTRIBUTES_A = IHostAttributes.build(new HostAttributes().setMode(NONE).setHost(HOST_A)); private static final HostOffer OFFER_A = new HostOffer( Offers.makeOffer("OFFER_A", HOST_A), HOST_ATTRIBUTES_A); private static final Protos.OfferID OFFER_A_ID = OFFER_A.getOffer().getId(); private static final String HOST_B = "HOST_B"; private static final HostOffer OFFER_B = new HostOffer( Offers.makeOffer("OFFER_B", HOST_B), IHostAttributes.build(new HostAttributes().setMode(NONE))); private static final String HOST_C = "HOST_C"; private static final HostOffer OFFER_C = new HostOffer( Offers.makeOffer("OFFER_C", HOST_C), IHostAttributes.build(new HostAttributes().setMode(NONE))); private static final int PORT = 1000; private static final Protos.Offer MESOS_OFFER = offer(mesosRange(PORTS, PORT)); 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(Protos.TaskID.newBuilder().setValue(Tasks.id(TASK))) .setAgentId(MESOS_OFFER.getAgentId()) .build(); private static Operation launch = Operation.newBuilder() .setType(Operation.Type.LAUNCH) .setLaunch(Operation.Launch.newBuilder().addTaskInfos(TASK_INFO)) .build(); private static final List<Operation> OPERATIONS = ImmutableList.of(launch); private static final long OFFER_FILTER_SECONDS = 0L; private static final Filters OFFER_FILTER = Filters.newBuilder() .setRefuseSeconds(OFFER_FILTER_SECONDS) .build(); private Driver driver; private FakeScheduledExecutor clock; private OfferManagerImpl offerManager; private FakeStatsProvider statsProvider; @Before public void setUp() { driver = createMock(Driver.class); DelayExecutor executorMock = createMock(DelayExecutor.class); clock = FakeScheduledExecutor.fromDelayExecutor(executorMock); addTearDown(clock::assertEmpty); OfferSettings offerSettings = new OfferSettings( Amount.of(OFFER_FILTER_SECONDS, Time.SECONDS), () -> RETURN_DELAY); statsProvider = new FakeStatsProvider(); offerManager = new OfferManagerImpl(driver, offerSettings, statsProvider, executorMock); } @Test public void testOffersSortedByUnavailability() throws Exception { clock.advance(Amount.of(1L, Time.HOURS)); HostOffer hostOfferB = setUnavailability(OFFER_B, clock.nowMillis()); Long offerCStartTime = clock.nowMillis() + ONE_HOUR.as(Time.MILLISECONDS); HostOffer hostOfferC = setUnavailability(OFFER_C, offerCStartTime); driver.declineOffer(OFFER_B.getOffer().getId(), OFFER_FILTER); driver.declineOffer(OFFER_A.getOffer().getId(), OFFER_FILTER); driver.declineOffer(OFFER_C.getOffer().getId(), OFFER_FILTER); control.replay(); offerManager.addOffer(hostOfferB); offerManager.addOffer(OFFER_A); offerManager.addOffer(hostOfferC); List<HostOffer> actual = ImmutableList.copyOf(offerManager.getOffers()); assertEquals( // hostOfferC has a further away start time, so it should be preferred. ImmutableList.of(OFFER_A, hostOfferC, hostOfferB), actual); clock.advance(RETURN_DELAY); } @Test public void testOffersSortedByMaintenance() throws Exception { // Ensures that non-DRAINING offers are preferred - the DRAINING offer would be tried last. HostOffer offerA = setMode(OFFER_A, DRAINING); HostOffer offerC = setMode(OFFER_C, DRAINING); driver.acceptOffers(OFFER_B.getOffer().getId(), OPERATIONS, OFFER_FILTER); expectLastCall(); driver.declineOffer(OFFER_A_ID, OFFER_FILTER); expectLastCall(); driver.declineOffer(offerC.getOffer().getId(), OFFER_FILTER); expectLastCall(); control.replay(); offerManager.addOffer(offerA); assertEquals(1L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); offerManager.addOffer(OFFER_B); assertEquals(2L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); offerManager.addOffer(offerC); assertEquals(3L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); assertEquals( ImmutableSet.of(OFFER_B, offerA, offerC), ImmutableSet.copyOf(offerManager.getOffers())); offerManager.launchTask(OFFER_B.getOffer().getId(), TASK_INFO); assertEquals(2L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); clock.advance(RETURN_DELAY); assertEquals(0L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); } @Test public void hostAttributeChangeUpdatesOfferSorting() throws Exception { driver.declineOffer(OFFER_A_ID, OFFER_FILTER); expectLastCall(); driver.declineOffer(OFFER_B.getOffer().getId(), OFFER_FILTER); expectLastCall(); control.replay(); offerManager.hostAttributesChanged(new HostAttributesChanged(HOST_ATTRIBUTES_A)); offerManager.addOffer(OFFER_A); offerManager.addOffer(OFFER_B); assertEquals(ImmutableSet.of(OFFER_A, OFFER_B), ImmutableSet.copyOf(offerManager.getOffers())); HostOffer offerA = setMode(OFFER_A, DRAINING); offerManager.hostAttributesChanged(new HostAttributesChanged(offerA.getAttributes())); assertEquals(ImmutableSet.of(OFFER_B, offerA), ImmutableSet.copyOf(offerManager.getOffers())); offerA = setMode(OFFER_A, NONE); HostOffer offerB = setMode(OFFER_B, DRAINING); offerManager.hostAttributesChanged(new HostAttributesChanged(offerA.getAttributes())); offerManager.hostAttributesChanged(new HostAttributesChanged(offerB.getAttributes())); assertEquals(ImmutableSet.of(OFFER_A, OFFER_B), ImmutableSet.copyOf(offerManager.getOffers())); clock.advance(RETURN_DELAY); } @Test public void testAddSameSlaveOffer() { driver.declineOffer(OFFER_A_ID, OFFER_FILTER); expectLastCall().times(2); control.replay(); offerManager.addOffer(OFFER_A); assertEquals(1L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); offerManager.addOffer(OFFER_A); assertEquals(0L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); clock.advance(RETURN_DELAY); } @Test public void testGetOffersReturnsAllOffers() throws Exception { control.replay(); offerManager.addOffer(OFFER_A); assertEquals(OFFER_A, Iterables.getOnlyElement(offerManager.getOffers())); assertEquals(1L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); offerManager.cancelOffer(OFFER_A_ID); assertTrue(Iterables.isEmpty(offerManager.getOffers())); assertEquals(0L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); clock.advance(RETURN_DELAY); } @Test public void testOfferFilteringDueToStaticBan() throws Exception { driver.declineOffer(OFFER_A_ID, OFFER_FILTER); expectLastCall(); control.replay(); // Static ban ignored when now offers. offerManager.banOffer(OFFER_A_ID, GROUP_KEY); assertEquals(0L, statsProvider.getLongValue(STATICALLY_BANNED_OFFERS)); offerManager.addOffer(OFFER_A); assertEquals(OFFER_A, Iterables.getOnlyElement(offerManager.getOffers(GROUP_KEY))); assertEquals(OFFER_A, Iterables.getOnlyElement(offerManager.getOffers())); // Add static ban. offerManager.banOffer(OFFER_A_ID, GROUP_KEY); assertEquals(1L, statsProvider.getLongValue(STATICALLY_BANNED_OFFERS)); assertEquals(OFFER_A, Iterables.getOnlyElement(offerManager.getOffers())); assertTrue(Iterables.isEmpty(offerManager.getOffers(GROUP_KEY))); clock.advance(RETURN_DELAY); assertEquals(0L, statsProvider.getLongValue(STATICALLY_BANNED_OFFERS)); } @Test public void testStaticBanIsClearedOnOfferReturn() throws Exception { driver.declineOffer(OFFER_A_ID, OFFER_FILTER); expectLastCall().times(2); control.replay(); offerManager.addOffer(OFFER_A); offerManager.banOffer(OFFER_A_ID, GROUP_KEY); assertEquals(OFFER_A, Iterables.getOnlyElement(offerManager.getOffers())); assertTrue(Iterables.isEmpty(offerManager.getOffers(GROUP_KEY))); assertEquals(1L, statsProvider.getLongValue(STATICALLY_BANNED_OFFERS)); // Make sure the static ban is cleared when the offers are returned. clock.advance(RETURN_DELAY); offerManager.addOffer(OFFER_A); assertEquals(OFFER_A, Iterables.getOnlyElement(offerManager.getOffers(GROUP_KEY))); assertEquals(0L, statsProvider.getLongValue(STATICALLY_BANNED_OFFERS)); clock.advance(RETURN_DELAY); } @Test public void testStaticBanIsClearedOnDriverDisconnect() throws Exception { driver.declineOffer(OFFER_A_ID, OFFER_FILTER); expectLastCall(); control.replay(); offerManager.addOffer(OFFER_A); offerManager.banOffer(OFFER_A_ID, GROUP_KEY); assertEquals(OFFER_A, Iterables.getOnlyElement(offerManager.getOffers())); assertTrue(Iterables.isEmpty(offerManager.getOffers(GROUP_KEY))); assertEquals(1L, statsProvider.getLongValue(STATICALLY_BANNED_OFFERS)); // Make sure the static ban is cleared when driver is disconnected. offerManager.driverDisconnected(new DriverDisconnected()); assertEquals(0L, statsProvider.getLongValue(STATICALLY_BANNED_OFFERS)); offerManager.addOffer(OFFER_A); assertEquals(OFFER_A, Iterables.getOnlyElement(offerManager.getOffers(GROUP_KEY))); clock.advance(RETURN_DELAY); } @Test public void getOffer() { driver.declineOffer(OFFER_A_ID, OFFER_FILTER); expectLastCall(); control.replay(); offerManager.addOffer(OFFER_A); assertEquals(Optional.of(OFFER_A), offerManager.getOffer(OFFER_A.getOffer().getAgentId())); assertEquals(1L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); clock.advance(RETURN_DELAY); } @Test(expected = OfferManager.LaunchException.class) public void testAcceptOffersDriverThrows() throws OfferManager.LaunchException { driver.acceptOffers(OFFER_A_ID, OPERATIONS, OFFER_FILTER); expectLastCall().andThrow(new IllegalStateException()); control.replay(); offerManager.addOffer(OFFER_A); try { offerManager.launchTask(OFFER_A_ID, TASK_INFO); } finally { clock.advance(RETURN_DELAY); } } @Test public void testLaunchTaskOfferRaceThrows() { control.replay(); try { offerManager.launchTask(OFFER_A_ID, TASK_INFO); fail("Method invocation is expected to throw exception."); } catch (OfferManager.LaunchException e) { assertEquals(1L, statsProvider.getLongValue(OFFER_ACCEPT_RACES)); } } @Test public void testFlushOffers() throws Exception { control.replay(); offerManager.addOffer(OFFER_A); offerManager.addOffer(OFFER_B); assertEquals(2L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); offerManager.driverDisconnected(new DriverDisconnected()); assertEquals(0L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); clock.advance(RETURN_DELAY); } @Test public void testDeclineOffer() throws Exception { driver.declineOffer(OFFER_A.getOffer().getId(), OFFER_FILTER); expectLastCall(); control.replay(); offerManager.addOffer(OFFER_A); assertEquals(1L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); clock.advance(RETURN_DELAY); assertEquals(0L, statsProvider.getLongValue(OUTSTANDING_OFFERS)); } private static HostOffer setUnavailability(HostOffer offer, Long startMs) { Unavailability unavailability = Unavailability.newBuilder() .setStart(TimeInfo.newBuilder().setNanoseconds(startMs * 1000L)).build(); return new HostOffer( offer.getOffer().toBuilder().setUnavailability(unavailability).build(), offer.getAttributes()); } private static HostOffer setMode(HostOffer offer, MaintenanceMode mode) { return new HostOffer( offer.getOffer(), IHostAttributes.build(offer.getAttributes().newBuilder().setMode(mode))); } }