From 17ad52c916dfd395e090f352f6342a2e7f378a96 Mon Sep 17 00:00:00 2001 From: "Tony L. Kerz" Date: Fri, 9 Oct 2015 08:19:19 -0400 Subject: [PATCH] [constraints] by hostname --- docs/docs/api.md | 5 +- .../scheduler/mesos/ConstraintChecker.scala | 35 ++++++++ .../scheduler/mesos/MesosJobFramework.scala | 14 +-- .../mesos/ConstraintCheckerSpec.scala | 85 +++++++++++++++++++ 4 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 src/main/scala/org/apache/mesos/chronos/scheduler/mesos/ConstraintChecker.scala create mode 100644 src/test/scala/org/apache/mesos/chronos/scheduler/mesos/ConstraintCheckerSpec.scala diff --git a/docs/docs/api.md b/docs/docs/api.md index d2ef6ad4d..5ac980574 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -387,8 +387,9 @@ When specifying the `command` field in your job hash, use `url-runner.bash` (mak ## Constraints -These constraints will currently only work against attributes that are specifically set on the Mesos slaves [as described in the Mesos documentation](http://mesos.apache.org/documentation/latest/configuration). -(i.e. the `hostname` attribute is not currently automatically available for constraints [as it is in Marathon](https://mesosphere.github.io/marathon/docs/constraints)) +These constraints will work against attributes that are specifically set on the Mesos slaves [as described in the Mesos documentation](http://mesos.apache.org/documentation/latest/configuration). + +If a `hostname` attribute is not explicitly specified, one will automatically be created and made available for constraints [as it is in Marathon](https://mesosphere.github.io/marathon/docs/constraints). It should be noted that calling out specific hostnames is not resilient to slave failure and should be avoided if possible. ### EQUALS constraint diff --git a/src/main/scala/org/apache/mesos/chronos/scheduler/mesos/ConstraintChecker.scala b/src/main/scala/org/apache/mesos/chronos/scheduler/mesos/ConstraintChecker.scala new file mode 100644 index 000000000..2b83cebdb --- /dev/null +++ b/src/main/scala/org/apache/mesos/chronos/scheduler/mesos/ConstraintChecker.scala @@ -0,0 +1,35 @@ +package org.apache.mesos.chronos.scheduler.mesos + +import org.apache.mesos.Protos +import org.apache.mesos.chronos.scheduler.jobs.constraints.Constraint +import java.util.logging.Logger +import scala.collection.JavaConverters._ + +/** + * Helper for checking resource offer against job constraints + * + * @author tony kerz (anthony.kerz@gmail.com) + */ +class ConstraintChecker { + private[this] val log = Logger.getLogger(getClass.getName) + val Hostname = "hostname" + + def checkConstraints(offer: Protos.Offer, constraints: Seq[Constraint]): Boolean = { + var attributes = offer.getAttributesList.asScala + + if (!attributes.exists(attr => attr.getName == Hostname)) { + log.fine(s"adding hostname-attribute=${offer.getHostname} to offer=${offer}") + val hostnameText = Protos.Value.Text.newBuilder().setValue(offer.getHostname).build() + val hostnameAttribute = Protos.Attribute.newBuilder().setName(Hostname).setText(hostnameText).setType(Protos.Value.Type.TEXT).build() + attributes = offer.getAttributesList.asScala :+ hostnameAttribute + } + + constraints.foreach { c => + if (!c.matches(attributes)) { + return false + } + } + true + } + +} diff --git a/src/main/scala/org/apache/mesos/chronos/scheduler/mesos/MesosJobFramework.scala b/src/main/scala/org/apache/mesos/chronos/scheduler/mesos/MesosJobFramework.scala index 008308782..d2dba0ca7 100644 --- a/src/main/scala/org/apache/mesos/chronos/scheduler/mesos/MesosJobFramework.scala +++ b/src/main/scala/org/apache/mesos/chronos/scheduler/mesos/MesosJobFramework.scala @@ -28,7 +28,8 @@ class MesosJobFramework @Inject()( val config: SchedulerConfiguration, val frameworkIdUtil: FrameworkIdUtil, val taskBuilder: MesosTaskBuilder, - val mesosOfferReviver: MesosOfferReviver) + val mesosOfferReviver: MesosOfferReviver, + val constraintChecker: ConstraintChecker = new ConstraintChecker()) extends Scheduler { val frameworkName = "chronos" @@ -116,15 +117,6 @@ class MesosJobFramework @Inject()( def generateLaunchableTasks(offerResources: mutable.HashMap[Offer, Resources]): mutable.Buffer[(String, BaseJob, Offer)] = { val tasks = mutable.Buffer[(String, BaseJob, Offer)]() - def checkConstraints(attributes: Seq[Protos.Attribute], constraints: Seq[Constraint]): Boolean = { - constraints.foreach { c => - if (!c.matches(attributes)) { - return false - } - } - true - } - @tailrec def generate() { taskManager.getTask match { @@ -143,7 +135,7 @@ class MesosJobFramework @Inject()( case None => val neededResources = new Resources(job) offerResources.toIterator.find { ors => - ors._2.canSatisfy(neededResources) && checkConstraints(ors._1.getAttributesList.asScala, job.constraints) + ors._2.canSatisfy(neededResources) && constraintChecker.checkConstraints(ors._1, job.constraints) } match { case Some((offer, resources)) => // Subtract this job's resource requirements from the remaining available resources in this offer. diff --git a/src/test/scala/org/apache/mesos/chronos/scheduler/mesos/ConstraintCheckerSpec.scala b/src/test/scala/org/apache/mesos/chronos/scheduler/mesos/ConstraintCheckerSpec.scala new file mode 100644 index 000000000..31f6b44be --- /dev/null +++ b/src/test/scala/org/apache/mesos/chronos/scheduler/mesos/ConstraintCheckerSpec.scala @@ -0,0 +1,85 @@ +package org.apache.mesos.chronos.scheduler.mesos + +import mesosphere.mesos.protos._ +import org.apache.mesos.chronos.scheduler.jobs.constraints.ConstraintSpecHelper +import org.apache.mesos.Protos +import java.util.logging.Logger +import org.specs2.mock.Mockito +import org.specs2.mutable.SpecificationWithJUnit +import org.apache.mesos.chronos.scheduler.jobs.constraints.LikeConstraint +import org.apache.mesos.chronos.scheduler.jobs.constraints.EqualsConstraint +import mesosphere.mesos.protos.Implicits._ +import org.specs2.specification.BeforeEach + +/** + * spec for testing ConstraintChecker + * + * @author tony kerz (anthony.kerz@gmail.com) + */ +class ConstraintCheckerSpec extends SpecificationWithJUnit +with Mockito +with ConstraintSpecHelper +{ + isolated + + val offer = Protos.Offer.newBuilder() + .setId(OfferID("1")) + .setFrameworkId(FrameworkID("chronos")) + .setSlaveId(SlaveID("slave-1")) + .setHostname("slave.one.com") + .addAttributes(createTextAttribute("rack", "rack-1")) + .build() + + val offerWithHostname = Protos.Offer.newBuilder() + .setId(OfferID("1")) + .setFrameworkId(FrameworkID("chronos")) + .setSlaveId(SlaveID("slave-1")) + .setHostname("slave.one.com") + .addAttributes(createTextAttribute("hostname", "slave.explicit.com")) + .build() + + val constraintChecker = new ConstraintChecker() + + "check constraints" should { + + "be true when equal" in { + val constraints = Seq(EqualsConstraint("rack", "rack-1")) + constraintChecker.checkConstraints(offer, constraints) must beTrue + } + + "be false when not equal" in { + val constraints = Seq(EqualsConstraint("rack", "rack-2")) + constraintChecker.checkConstraints(offer, constraints) must beFalse + } + + "be true when like" in { + val constraints = Seq(LikeConstraint("rack", "rack-[1-3]")) + constraintChecker.checkConstraints(offer, constraints) must beTrue + } + + "be false when not like" in { + val constraints = Seq(LikeConstraint("rack", "rack-[2-3]")) + constraintChecker.checkConstraints(offer, constraints) must beFalse + } + + "be true when hostname equal" in { + val constraints = Seq(EqualsConstraint("hostname", "slave.one.com")) + constraintChecker.checkConstraints(offer, constraints) must beTrue + } + + "be false when hostname not equal" in { + val constraints = Seq(EqualsConstraint("hostname", "slave.two.com")) + constraintChecker.checkConstraints(offer, constraints) must beFalse + } + + "be false when hostname explicitly set to something else and not equal" in { + val constraints = Seq(EqualsConstraint("hostname", "slave.one.com")) + constraintChecker.checkConstraints(offerWithHostname, constraints) must beFalse + } + + "be true when hostname explicitly set to something else and equal" in { + val constraints = Seq(EqualsConstraint("hostname", "slave.explicit.com")) + constraintChecker.checkConstraints(offerWithHostname, constraints) must beTrue + } + } +}