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