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