Skip to content

Commit

Permalink
Refactor mesos scheduler resourceOffers and add unit test
Browse files Browse the repository at this point in the history
  • Loading branch information
tnachen committed Nov 11, 2014
1 parent 7f37188 commit 8207428
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ private[spark] class MesosSchedulerBackend(
}
}

def toWorkerOffer(offer: Offer) = new WorkerOffer(
offer.getSlaveId.getValue,
offer.getHostname,
getResource(offer.getResourcesList, "cpus").toInt)

override def disconnected(d: SchedulerDriver) {}

override def reregistered(d: SchedulerDriver, masterInfo: MasterInfo) {}
Expand All @@ -212,62 +217,39 @@ private[spark] class MesosSchedulerBackend(
override def resourceOffers(d: SchedulerDriver, offers: JList[Offer]) {
val oldClassLoader = setClassLoader()
try {
synchronized {
// Build a big list of the offerable workers, and remember their indices so that we can
// figure out which Offer to reply to for each worker
val offerableWorkers = new ArrayBuffer[WorkerOffer]
val offerableIndices = new HashMap[String, Int]

def sufficientOffer(o: Offer) = {
val mem = getResource(o.getResourcesList, "mem")
val cpus = getResource(o.getResourcesList, "cpus")
val slaveId = o.getSlaveId.getValue
(mem >= MemoryUtils.calculateTotalMemory(sc) &&
// need at least 1 for executor, 1 for task
cpus >= 2 * scheduler.CPUS_PER_TASK) ||
(slaveIdsWithExecutors.contains(slaveId) &&
cpus >= scheduler.CPUS_PER_TASK)
}

for ((offer, index) <- offers.zipWithIndex if sufficientOffer(offer)) {
val slaveId = offer.getSlaveId.getValue
offerableIndices.put(slaveId, index)
val cpus = if (slaveIdsWithExecutors.contains(slaveId)) {
getResource(offer.getResourcesList, "cpus").toInt
} else {
// If the executor doesn't exist yet, subtract CPU for executor
getResource(offer.getResourcesList, "cpus").toInt -
scheduler.CPUS_PER_TASK
}
offerableWorkers += new WorkerOffer(
offer.getSlaveId.getValue,
offer.getHostname,
cpus)
}

// Call into the TaskSchedulerImpl
val taskLists = scheduler.resourceOffers(offerableWorkers)

// Build a list of Mesos tasks for each slave
val mesosTasks = offers.map(o => new JArrayList[MesosTaskInfo]())
for ((taskList, index) <- taskLists.zipWithIndex) {
if (!taskList.isEmpty) {
for (taskDesc <- taskList) {
val slaveId = taskDesc.executorId
val offerNum = offerableIndices(slaveId)
slaveIdsWithExecutors += slaveId
taskIdToSlaveId(taskDesc.taskId) = slaveId
mesosTasks(offerNum).add(createMesosTask(taskDesc, slaveId))
}
}
}

// Reply to the offers
val filters = Filters.newBuilder().setRefuseSeconds(1).build() // TODO: lower timeout?
for (i <- 0 until offers.size) {
d.launchTasks(Collections.singleton(offers(i).getId), mesosTasks(i), filters)
val (acceptedOffers, declinedOffers) = offers.partition(o => {
val mem = getResource(o.getResourcesList, "mem")
val slaveId = o.getSlaveId.getValue
mem >= sc.executorMemory || slaveIdsWithExecutors.contains(slaveId)
})

val offerableWorkers = acceptedOffers.map(toWorkerOffer)

val slaveIdToOffer = acceptedOffers.map(o => o.getSlaveId.getValue -> o).toMap

val mesosTasks = new HashMap[String, JArrayList[MesosTaskInfo]]

// Call into the TaskSchedulerImpl
scheduler.resourceOffers(offerableWorkers)
.filter(!_.isEmpty)
.foreach(_.foreach(taskDesc => {
val slaveId = taskDesc.executorId
slaveIdsWithExecutors += slaveId
taskIdToSlaveId(taskDesc.taskId) = slaveId
mesosTasks.getOrElseUpdate(slaveId, new JArrayList[MesosTaskInfo])
.add(createMesosTask(taskDesc, slaveId))
}))

// Reply to the offers
val filters = Filters.newBuilder().setRefuseSeconds(1).build() // TODO: lower timeout?

mesosTasks.foreach {
case (slaveId, tasks) => {
d.launchTasks(Collections.singleton(slaveIdToOffer(slaveId).getId), tasks, filters)
}
}

declinedOffers.foreach(o => d.declineOffer(o.getId))
} finally {
restoreClassLoader(oldClassLoader)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.spark.scheduler.mesos

import org.scalatest.FunSuite
import org.apache.spark.{SparkConf, SparkContext, LocalSparkContext}
import org.apache.spark.scheduler.{TaskDescription, WorkerOffer, TaskSchedulerImpl}
import org.apache.spark.scheduler.cluster.mesos.MesosSchedulerBackend
import org.apache.mesos.SchedulerDriver
import org.apache.mesos.Protos._
import org.scalatest.mock.EasyMockSugar
import org.apache.mesos.Protos.Value.Scalar
import org.easymock.{Capture, EasyMock}
import java.nio.ByteBuffer
import java.util.Collections
import java.util

class MesosSchedulerBackendSuite extends FunSuite with LocalSparkContext with EasyMockSugar {
test("mesos resource offer is launching tasks") {
def createOffer(id: Int, mem: Int, cpu: Int) = {
val builder = Offer.newBuilder()
builder.addResourcesBuilder()
.setName("mem")
.setType(Value.Type.SCALAR)
.setScalar(Scalar.newBuilder().setValue(mem))
builder.addResourcesBuilder()
.setName("cpus")
.setType(Value.Type.SCALAR)
.setScalar(Scalar.newBuilder().setValue(cpu))
builder.setId(OfferID.newBuilder().setValue(id.toString).build()).setFrameworkId(FrameworkID.newBuilder().setValue("f1"))
.setSlaveId(SlaveID.newBuilder().setValue("s1")).setHostname("localhost").build()
}

val driver = EasyMock.createMock(classOf[SchedulerDriver])
val taskScheduler = EasyMock.createMock(classOf[TaskSchedulerImpl])
val offers = new java.util.ArrayList[Offer]
offers.add(createOffer(1, 101, 1))
offers.add(createOffer(1, 99, 1))

val conf = new SparkConf
conf.set("spark.executor.memory", "100m")
conf.set("spark.home", "/path")
val sc = new SparkContext("local-cluster[2 , 1 , 512]", "test", conf)
val backend = new MesosSchedulerBackend(taskScheduler, sc, "master")
val workerOffers = Seq(backend.toWorkerOffer(offers.get(0)))
val taskDesc = new TaskDescription(1L, "s1", "n1", 0, ByteBuffer.wrap(new Array[Byte](0)))
EasyMock.expect(taskScheduler.resourceOffers(EasyMock.eq(workerOffers))).andReturn(Seq(Seq(taskDesc)))
EasyMock.expect(taskScheduler.CPUS_PER_TASK).andReturn(2).anyTimes()
EasyMock.replay(taskScheduler)
val capture = new Capture[util.Collection[TaskInfo]]
EasyMock.expect(
driver.launchTasks(
EasyMock.eq(Collections.singleton(offers.get(0).getId)),
EasyMock.capture(capture),
EasyMock.anyObject(classOf[Filters])
)
).andReturn(Status.valueOf(1))
EasyMock.expect(driver.declineOffer(offers.get(1).getId)).andReturn(Status.valueOf(1))
EasyMock.replay(driver)
backend.resourceOffers(driver, offers)
assert(capture.getValue.size() == 1)
val taskInfo = capture.getValue.iterator().next()
assert(taskInfo.getName.equals("n1"))
val cpus = taskInfo.getResourcesList.get(0)
assert(cpus.getName.equals("cpus"))
assert(cpus.getScalar.getValue.equals(2.0))
assert(taskInfo.getSlaveId.getValue.equals("s1"))
}
}

0 comments on commit 8207428

Please sign in to comment.