github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/scheduler/feasible.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"regexp"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/go-version"
    11  	"github.com/hashicorp/nomad/nomad/structs"
    12  )
    13  
    14  // FeasibleIterator is used to iteratively yield nodes that
    15  // match feasibility constraints. The iterators may manage
    16  // some state for performance optimizations.
    17  type FeasibleIterator interface {
    18  	// Next yields a feasible node or nil if exhausted
    19  	Next() *structs.Node
    20  
    21  	// Reset is invoked when an allocation has been placed
    22  	// to reset any stale state.
    23  	Reset()
    24  }
    25  
    26  // FeasibilityChecker is used to check if a single node meets feasibility
    27  // constraints.
    28  type FeasibilityChecker interface {
    29  	Feasible(*structs.Node) bool
    30  }
    31  
    32  // StaticIterator is a FeasibleIterator which returns nodes
    33  // in a static order. This is used at the base of the iterator
    34  // chain only for testing due to deterministic behavior.
    35  type StaticIterator struct {
    36  	ctx    Context
    37  	nodes  []*structs.Node
    38  	offset int
    39  	seen   int
    40  }
    41  
    42  // NewStaticIterator constructs a random iterator from a list of nodes
    43  func NewStaticIterator(ctx Context, nodes []*structs.Node) *StaticIterator {
    44  	iter := &StaticIterator{
    45  		ctx:   ctx,
    46  		nodes: nodes,
    47  	}
    48  	return iter
    49  }
    50  
    51  func (iter *StaticIterator) Next() *structs.Node {
    52  	// Check if exhausted
    53  	n := len(iter.nodes)
    54  	if iter.offset == n || iter.seen == n {
    55  		if iter.seen != n {
    56  			iter.offset = 0
    57  		} else {
    58  			return nil
    59  		}
    60  	}
    61  
    62  	// Return the next offset
    63  	offset := iter.offset
    64  	iter.offset += 1
    65  	iter.seen += 1
    66  	iter.ctx.Metrics().EvaluateNode()
    67  	return iter.nodes[offset]
    68  }
    69  
    70  func (iter *StaticIterator) Reset() {
    71  	iter.seen = 0
    72  }
    73  
    74  func (iter *StaticIterator) SetNodes(nodes []*structs.Node) {
    75  	iter.nodes = nodes
    76  	iter.offset = 0
    77  	iter.seen = 0
    78  }
    79  
    80  // NewRandomIterator constructs a static iterator from a list of nodes
    81  // after applying the Fisher-Yates algorithm for a random shuffle. This
    82  // is applied in-place
    83  func NewRandomIterator(ctx Context, nodes []*structs.Node) *StaticIterator {
    84  	// shuffle with the Fisher-Yates algorithm
    85  	shuffleNodes(nodes)
    86  
    87  	// Create a static iterator
    88  	return NewStaticIterator(ctx, nodes)
    89  }
    90  
    91  // DriverChecker is a FeasibilityChecker which returns whether a node has the
    92  // drivers necessary to scheduler a task group.
    93  type DriverChecker struct {
    94  	ctx     Context
    95  	drivers map[string]struct{}
    96  }
    97  
    98  // NewDriverChecker creates a DriverChecker from a set of drivers
    99  func NewDriverChecker(ctx Context, drivers map[string]struct{}) *DriverChecker {
   100  	return &DriverChecker{
   101  		ctx:     ctx,
   102  		drivers: drivers,
   103  	}
   104  }
   105  
   106  func (c *DriverChecker) SetDrivers(d map[string]struct{}) {
   107  	c.drivers = d
   108  }
   109  
   110  func (c *DriverChecker) Feasible(option *structs.Node) bool {
   111  	// Use this node if possible
   112  	if c.hasDrivers(option) {
   113  		return true
   114  	}
   115  	c.ctx.Metrics().FilterNode(option, "missing drivers")
   116  	return false
   117  }
   118  
   119  // hasDrivers is used to check if the node has all the appropriate
   120  // drivers for this task group. Drivers are registered as node attribute
   121  // like "driver.docker=1" with their corresponding version.
   122  func (c *DriverChecker) hasDrivers(option *structs.Node) bool {
   123  	for driver := range c.drivers {
   124  		driverStr := fmt.Sprintf("driver.%s", driver)
   125  		value, ok := option.Attributes[driverStr]
   126  		if !ok {
   127  			return false
   128  		}
   129  
   130  		enabled, err := strconv.ParseBool(value)
   131  		if err != nil {
   132  			c.ctx.Logger().
   133  				Printf("[WARN] scheduler.DriverChecker: node %v has invalid driver setting %v: %v",
   134  					option.ID, driverStr, value)
   135  			return false
   136  		}
   137  
   138  		if !enabled {
   139  			return false
   140  		}
   141  	}
   142  	return true
   143  }
   144  
   145  // ProposedAllocConstraintIterator is a FeasibleIterator which returns nodes that
   146  // match constraints that are not static such as Node attributes but are
   147  // effected by proposed alloc placements. Examples are distinct_hosts and
   148  // tenancy constraints. This is used to filter on job and task group
   149  // constraints.
   150  type ProposedAllocConstraintIterator struct {
   151  	ctx    Context
   152  	source FeasibleIterator
   153  	tg     *structs.TaskGroup
   154  	job    *structs.Job
   155  
   156  	// Store whether the Job or TaskGroup has a distinct_hosts constraints so
   157  	// they don't have to be calculated every time Next() is called.
   158  	tgDistinctHosts  bool
   159  	jobDistinctHosts bool
   160  }
   161  
   162  // NewProposedAllocConstraintIterator creates a ProposedAllocConstraintIterator
   163  // from a source.
   164  func NewProposedAllocConstraintIterator(ctx Context, source FeasibleIterator) *ProposedAllocConstraintIterator {
   165  	return &ProposedAllocConstraintIterator{
   166  		ctx:    ctx,
   167  		source: source,
   168  	}
   169  }
   170  
   171  func (iter *ProposedAllocConstraintIterator) SetTaskGroup(tg *structs.TaskGroup) {
   172  	iter.tg = tg
   173  	iter.tgDistinctHosts = iter.hasDistinctHostsConstraint(tg.Constraints)
   174  }
   175  
   176  func (iter *ProposedAllocConstraintIterator) SetJob(job *structs.Job) {
   177  	iter.job = job
   178  	iter.jobDistinctHosts = iter.hasDistinctHostsConstraint(job.Constraints)
   179  }
   180  
   181  func (iter *ProposedAllocConstraintIterator) hasDistinctHostsConstraint(constraints []*structs.Constraint) bool {
   182  	for _, con := range constraints {
   183  		if con.Operand == structs.ConstraintDistinctHosts {
   184  			return true
   185  		}
   186  	}
   187  	return false
   188  }
   189  
   190  func (iter *ProposedAllocConstraintIterator) Next() *structs.Node {
   191  	for {
   192  		// Get the next option from the source
   193  		option := iter.source.Next()
   194  
   195  		// Hot-path if the option is nil or there are no distinct_hosts constraints.
   196  		if option == nil || !(iter.jobDistinctHosts || iter.tgDistinctHosts) {
   197  			return option
   198  		}
   199  
   200  		if !iter.satisfiesDistinctHosts(option) {
   201  			iter.ctx.Metrics().FilterNode(option, structs.ConstraintDistinctHosts)
   202  			continue
   203  		}
   204  
   205  		return option
   206  	}
   207  }
   208  
   209  // satisfiesDistinctHosts checks if the node satisfies a distinct_hosts
   210  // constraint either specified at the job level or the TaskGroup level.
   211  func (iter *ProposedAllocConstraintIterator) satisfiesDistinctHosts(option *structs.Node) bool {
   212  	// Check if there is no constraint set.
   213  	if !(iter.jobDistinctHosts || iter.tgDistinctHosts) {
   214  		return true
   215  	}
   216  
   217  	// Get the proposed allocations
   218  	proposed, err := iter.ctx.ProposedAllocs(option.ID)
   219  	if err != nil {
   220  		iter.ctx.Logger().Printf(
   221  			"[ERR] scheduler.dynamic-constraint: failed to get proposed allocations: %v", err)
   222  		return false
   223  	}
   224  
   225  	// Skip the node if the task group has already been allocated on it.
   226  	for _, alloc := range proposed {
   227  		// If the job has a distinct_hosts constraint we only need an alloc
   228  		// collision on the JobID but if the constraint is on the TaskGroup then
   229  		// we need both a job and TaskGroup collision.
   230  		jobCollision := alloc.JobID == iter.job.ID
   231  		taskCollision := alloc.TaskGroup == iter.tg.Name
   232  		if iter.jobDistinctHosts && jobCollision || jobCollision && taskCollision {
   233  			return false
   234  		}
   235  	}
   236  
   237  	return true
   238  }
   239  
   240  func (iter *ProposedAllocConstraintIterator) Reset() {
   241  	iter.source.Reset()
   242  }
   243  
   244  // ConstraintChecker is a FeasibilityChecker which returns nodes that match a
   245  // given set of constraints. This is used to filter on job, task group, and task
   246  // constraints.
   247  type ConstraintChecker struct {
   248  	ctx         Context
   249  	constraints []*structs.Constraint
   250  }
   251  
   252  // NewConstraintChecker creates a ConstraintChecker for a set of constraints
   253  func NewConstraintChecker(ctx Context, constraints []*structs.Constraint) *ConstraintChecker {
   254  	return &ConstraintChecker{
   255  		ctx:         ctx,
   256  		constraints: constraints,
   257  	}
   258  }
   259  
   260  func (c *ConstraintChecker) SetConstraints(constraints []*structs.Constraint) {
   261  	c.constraints = constraints
   262  }
   263  
   264  func (c *ConstraintChecker) Feasible(option *structs.Node) bool {
   265  	// Use this node if possible
   266  	for _, constraint := range c.constraints {
   267  		if !c.meetsConstraint(constraint, option) {
   268  			c.ctx.Metrics().FilterNode(option, constraint.String())
   269  			return false
   270  		}
   271  	}
   272  	return true
   273  }
   274  
   275  func (c *ConstraintChecker) meetsConstraint(constraint *structs.Constraint, option *structs.Node) bool {
   276  	// Resolve the targets
   277  	lVal, ok := resolveConstraintTarget(constraint.LTarget, option)
   278  	if !ok {
   279  		return false
   280  	}
   281  	rVal, ok := resolveConstraintTarget(constraint.RTarget, option)
   282  	if !ok {
   283  		return false
   284  	}
   285  
   286  	// Check if satisfied
   287  	return checkConstraint(c.ctx, constraint.Operand, lVal, rVal)
   288  }
   289  
   290  // resolveConstraintTarget is used to resolve the LTarget and RTarget of a Constraint
   291  func resolveConstraintTarget(target string, node *structs.Node) (interface{}, bool) {
   292  	// If no prefix, this must be a literal value
   293  	if !strings.HasPrefix(target, "${") {
   294  		return target, true
   295  	}
   296  
   297  	// Handle the interpolations
   298  	switch {
   299  	case "${node.unique.id}" == target:
   300  		return node.ID, true
   301  
   302  	case "${node.datacenter}" == target:
   303  		return node.Datacenter, true
   304  
   305  	case "${node.unique.name}" == target:
   306  		return node.Name, true
   307  
   308  	case "${node.class}" == target:
   309  		return node.NodeClass, true
   310  
   311  	case strings.HasPrefix(target, "${attr."):
   312  		attr := strings.TrimSuffix(strings.TrimPrefix(target, "${attr."), "}")
   313  		val, ok := node.Attributes[attr]
   314  		return val, ok
   315  
   316  	case strings.HasPrefix(target, "${meta."):
   317  		meta := strings.TrimSuffix(strings.TrimPrefix(target, "${meta."), "}")
   318  		val, ok := node.Meta[meta]
   319  		return val, ok
   320  
   321  	default:
   322  		return nil, false
   323  	}
   324  }
   325  
   326  // checkConstraint checks if a constraint is satisfied
   327  func checkConstraint(ctx Context, operand string, lVal, rVal interface{}) bool {
   328  	// Check for constraints not handled by this checker.
   329  	switch operand {
   330  	case structs.ConstraintDistinctHosts:
   331  		return true
   332  	default:
   333  		break
   334  	}
   335  
   336  	switch operand {
   337  	case "=", "==", "is":
   338  		return reflect.DeepEqual(lVal, rVal)
   339  	case "!=", "not":
   340  		return !reflect.DeepEqual(lVal, rVal)
   341  	case "<", "<=", ">", ">=":
   342  		return checkLexicalOrder(operand, lVal, rVal)
   343  	case structs.ConstraintVersion:
   344  		return checkVersionConstraint(ctx, lVal, rVal)
   345  	case structs.ConstraintRegex:
   346  		return checkRegexpConstraint(ctx, lVal, rVal)
   347  	case structs.ConstraintSetContains:
   348  		return checkSetContainsConstraint(ctx, lVal, rVal)
   349  	default:
   350  		return false
   351  	}
   352  }
   353  
   354  // checkLexicalOrder is used to check for lexical ordering
   355  func checkLexicalOrder(op string, lVal, rVal interface{}) bool {
   356  	// Ensure the values are strings
   357  	lStr, ok := lVal.(string)
   358  	if !ok {
   359  		return false
   360  	}
   361  	rStr, ok := rVal.(string)
   362  	if !ok {
   363  		return false
   364  	}
   365  
   366  	switch op {
   367  	case "<":
   368  		return lStr < rStr
   369  	case "<=":
   370  		return lStr <= rStr
   371  	case ">":
   372  		return lStr > rStr
   373  	case ">=":
   374  		return lStr >= rStr
   375  	default:
   376  		return false
   377  	}
   378  }
   379  
   380  // checkVersionConstraint is used to compare a version on the
   381  // left hand side with a set of constraints on the right hand side
   382  func checkVersionConstraint(ctx Context, lVal, rVal interface{}) bool {
   383  	// Parse the version
   384  	var versionStr string
   385  	switch v := lVal.(type) {
   386  	case string:
   387  		versionStr = v
   388  	case int:
   389  		versionStr = fmt.Sprintf("%d", v)
   390  	default:
   391  		return false
   392  	}
   393  
   394  	// Parse the version
   395  	vers, err := version.NewVersion(versionStr)
   396  	if err != nil {
   397  		return false
   398  	}
   399  
   400  	// Constraint must be a string
   401  	constraintStr, ok := rVal.(string)
   402  	if !ok {
   403  		return false
   404  	}
   405  
   406  	// Check the cache for a match
   407  	cache := ctx.ConstraintCache()
   408  	constraints := cache[constraintStr]
   409  
   410  	// Parse the constraints
   411  	if constraints == nil {
   412  		constraints, err = version.NewConstraint(constraintStr)
   413  		if err != nil {
   414  			return false
   415  		}
   416  		cache[constraintStr] = constraints
   417  	}
   418  
   419  	// Check the constraints against the version
   420  	return constraints.Check(vers)
   421  }
   422  
   423  // checkRegexpConstraint is used to compare a value on the
   424  // left hand side with a regexp on the right hand side
   425  func checkRegexpConstraint(ctx Context, lVal, rVal interface{}) bool {
   426  	// Ensure left-hand is string
   427  	lStr, ok := lVal.(string)
   428  	if !ok {
   429  		return false
   430  	}
   431  
   432  	// Regexp must be a string
   433  	regexpStr, ok := rVal.(string)
   434  	if !ok {
   435  		return false
   436  	}
   437  
   438  	// Check the cache
   439  	cache := ctx.RegexpCache()
   440  	re := cache[regexpStr]
   441  
   442  	// Parse the regexp
   443  	if re == nil {
   444  		var err error
   445  		re, err = regexp.Compile(regexpStr)
   446  		if err != nil {
   447  			return false
   448  		}
   449  		cache[regexpStr] = re
   450  	}
   451  
   452  	// Look for a match
   453  	return re.MatchString(lStr)
   454  }
   455  
   456  // checkSetContainsConstraint is used to see if the left hand side contains the
   457  // string on the right hand side
   458  func checkSetContainsConstraint(ctx Context, lVal, rVal interface{}) bool {
   459  	// Ensure left-hand is string
   460  	lStr, ok := lVal.(string)
   461  	if !ok {
   462  		return false
   463  	}
   464  
   465  	// Regexp must be a string
   466  	rStr, ok := rVal.(string)
   467  	if !ok {
   468  		return false
   469  	}
   470  
   471  	input := strings.Split(lStr, ",")
   472  	lookup := make(map[string]struct{}, len(input))
   473  	for _, in := range input {
   474  		cleaned := strings.TrimSpace(in)
   475  		lookup[cleaned] = struct{}{}
   476  	}
   477  
   478  	for _, r := range strings.Split(rStr, ",") {
   479  		cleaned := strings.TrimSpace(r)
   480  		if _, ok := lookup[cleaned]; !ok {
   481  			return false
   482  		}
   483  	}
   484  
   485  	return true
   486  }
   487  
   488  // FeasibilityWrapper is a FeasibleIterator which wraps both job and task group
   489  // FeasibilityCheckers in which feasibility checking can be skipped if the
   490  // computed node class has previously been marked as eligible or ineligible.
   491  type FeasibilityWrapper struct {
   492  	ctx         Context
   493  	source      FeasibleIterator
   494  	jobCheckers []FeasibilityChecker
   495  	tgCheckers  []FeasibilityChecker
   496  	tg          string
   497  }
   498  
   499  // NewFeasibilityWrapper returns a FeasibleIterator based on the passed source
   500  // and FeasibilityCheckers.
   501  func NewFeasibilityWrapper(ctx Context, source FeasibleIterator,
   502  	jobCheckers, tgCheckers []FeasibilityChecker) *FeasibilityWrapper {
   503  	return &FeasibilityWrapper{
   504  		ctx:         ctx,
   505  		source:      source,
   506  		jobCheckers: jobCheckers,
   507  		tgCheckers:  tgCheckers,
   508  	}
   509  }
   510  
   511  func (w *FeasibilityWrapper) SetTaskGroup(tg string) {
   512  	w.tg = tg
   513  }
   514  
   515  func (w *FeasibilityWrapper) Reset() {
   516  	w.source.Reset()
   517  }
   518  
   519  // Next returns an eligible node, only running the FeasibilityCheckers as needed
   520  // based on the sources computed node class.
   521  func (w *FeasibilityWrapper) Next() *structs.Node {
   522  	evalElig := w.ctx.Eligibility()
   523  	metrics := w.ctx.Metrics()
   524  
   525  OUTER:
   526  	for {
   527  		// Get the next option from the source
   528  		option := w.source.Next()
   529  		if option == nil {
   530  			return nil
   531  		}
   532  
   533  		// Check if the job has been marked as eligible or ineligible.
   534  		jobEscaped, jobUnknown := false, false
   535  		switch evalElig.JobStatus(option.ComputedClass) {
   536  		case EvalComputedClassIneligible:
   537  			// Fast path the ineligible case
   538  			metrics.FilterNode(option, "computed class ineligible")
   539  			continue
   540  		case EvalComputedClassEscaped:
   541  			jobEscaped = true
   542  		case EvalComputedClassUnknown:
   543  			jobUnknown = true
   544  		}
   545  
   546  		// Run the job feasibility checks.
   547  		for _, check := range w.jobCheckers {
   548  			feasible := check.Feasible(option)
   549  			if !feasible {
   550  				// If the job hasn't escaped, set it to be ineligible since it
   551  				// failed a job check.
   552  				if !jobEscaped {
   553  					evalElig.SetJobEligibility(false, option.ComputedClass)
   554  				}
   555  				continue OUTER
   556  			}
   557  		}
   558  
   559  		// Set the job eligibility if the constraints weren't escaped and it
   560  		// hasn't been set before.
   561  		if !jobEscaped && jobUnknown {
   562  			evalElig.SetJobEligibility(true, option.ComputedClass)
   563  		}
   564  
   565  		// Check if the task group has been marked as eligible or ineligible.
   566  		tgEscaped, tgUnknown := false, false
   567  		switch evalElig.TaskGroupStatus(w.tg, option.ComputedClass) {
   568  		case EvalComputedClassIneligible:
   569  			// Fast path the ineligible case
   570  			metrics.FilterNode(option, "computed class ineligible")
   571  			continue
   572  		case EvalComputedClassEligible:
   573  			// Fast path the eligible case
   574  			return option
   575  		case EvalComputedClassEscaped:
   576  			tgEscaped = true
   577  		case EvalComputedClassUnknown:
   578  			tgUnknown = true
   579  		}
   580  
   581  		// Run the task group feasibility checks.
   582  		for _, check := range w.tgCheckers {
   583  			feasible := check.Feasible(option)
   584  			if !feasible {
   585  				// If the task group hasn't escaped, set it to be ineligible
   586  				// since it failed a check.
   587  				if !tgEscaped {
   588  					evalElig.SetTaskGroupEligibility(false, w.tg, option.ComputedClass)
   589  				}
   590  				continue OUTER
   591  			}
   592  		}
   593  
   594  		// Set the task group eligibility if the constraints weren't escaped and
   595  		// it hasn't been set before.
   596  		if !tgEscaped && tgUnknown {
   597  			evalElig.SetTaskGroupEligibility(true, w.tg, option.ComputedClass)
   598  		}
   599  
   600  		return option
   601  	}
   602  }