github.com/bigcommerce/nomad@v0.9.3-bc/scheduler/feasible.go (about) 1 package scheduler 2 3 import ( 4 "fmt" 5 "reflect" 6 "regexp" 7 "strconv" 8 "strings" 9 10 version "github.com/hashicorp/go-version" 11 "github.com/hashicorp/nomad/nomad/structs" 12 psstructs "github.com/hashicorp/nomad/plugins/shared/structs" 13 ) 14 15 // FeasibleIterator is used to iteratively yield nodes that 16 // match feasibility constraints. The iterators may manage 17 // some state for performance optimizations. 18 type FeasibleIterator interface { 19 // Next yields a feasible node or nil if exhausted 20 Next() *structs.Node 21 22 // Reset is invoked when an allocation has been placed 23 // to reset any stale state. 24 Reset() 25 } 26 27 // JobContextualIterator is an iterator that can have the job and task group set 28 // on it. 29 type ContextualIterator interface { 30 SetJob(*structs.Job) 31 SetTaskGroup(*structs.TaskGroup) 32 } 33 34 // FeasibilityChecker is used to check if a single node meets feasibility 35 // constraints. 36 type FeasibilityChecker interface { 37 Feasible(*structs.Node) bool 38 } 39 40 // StaticIterator is a FeasibleIterator which returns nodes 41 // in a static order. This is used at the base of the iterator 42 // chain only for testing due to deterministic behavior. 43 type StaticIterator struct { 44 ctx Context 45 nodes []*structs.Node 46 offset int 47 seen int 48 } 49 50 // NewStaticIterator constructs a random iterator from a list of nodes 51 func NewStaticIterator(ctx Context, nodes []*structs.Node) *StaticIterator { 52 iter := &StaticIterator{ 53 ctx: ctx, 54 nodes: nodes, 55 } 56 return iter 57 } 58 59 func (iter *StaticIterator) Next() *structs.Node { 60 // Check if exhausted 61 n := len(iter.nodes) 62 if iter.offset == n || iter.seen == n { 63 if iter.seen != n { 64 iter.offset = 0 65 } else { 66 return nil 67 } 68 } 69 70 // Return the next offset 71 offset := iter.offset 72 iter.offset += 1 73 iter.seen += 1 74 iter.ctx.Metrics().EvaluateNode() 75 return iter.nodes[offset] 76 } 77 78 func (iter *StaticIterator) Reset() { 79 iter.seen = 0 80 } 81 82 func (iter *StaticIterator) SetNodes(nodes []*structs.Node) { 83 iter.nodes = nodes 84 iter.offset = 0 85 iter.seen = 0 86 } 87 88 // NewRandomIterator constructs a static iterator from a list of nodes 89 // after applying the Fisher-Yates algorithm for a random shuffle. This 90 // is applied in-place 91 func NewRandomIterator(ctx Context, nodes []*structs.Node) *StaticIterator { 92 // shuffle with the Fisher-Yates algorithm 93 shuffleNodes(nodes) 94 95 // Create a static iterator 96 return NewStaticIterator(ctx, nodes) 97 } 98 99 // DriverChecker is a FeasibilityChecker which returns whether a node has the 100 // drivers necessary to scheduler a task group. 101 type DriverChecker struct { 102 ctx Context 103 drivers map[string]struct{} 104 } 105 106 // NewDriverChecker creates a DriverChecker from a set of drivers 107 func NewDriverChecker(ctx Context, drivers map[string]struct{}) *DriverChecker { 108 return &DriverChecker{ 109 ctx: ctx, 110 drivers: drivers, 111 } 112 } 113 114 func (c *DriverChecker) SetDrivers(d map[string]struct{}) { 115 c.drivers = d 116 } 117 118 func (c *DriverChecker) Feasible(option *structs.Node) bool { 119 // Use this node if possible 120 if c.hasDrivers(option) { 121 return true 122 } 123 c.ctx.Metrics().FilterNode(option, "missing drivers") 124 return false 125 } 126 127 // hasDrivers is used to check if the node has all the appropriate 128 // drivers for this task group. Drivers are registered as node attribute 129 // like "driver.docker=1" with their corresponding version. 130 func (c *DriverChecker) hasDrivers(option *structs.Node) bool { 131 for driver := range c.drivers { 132 driverStr := fmt.Sprintf("driver.%s", driver) 133 134 // COMPAT: Remove in 0.10: As of Nomad 0.8, nodes have a DriverInfo that 135 // corresponds with every driver. As a Nomad server might be on a later 136 // version than a Nomad client, we need to check for compatibility here 137 // to verify the client supports this. 138 if driverInfo, ok := option.Drivers[driver]; ok { 139 if driverInfo == nil { 140 c.ctx.Logger().Named("driver_checker").Warn("node has no driver info set", "node_id", option.ID, "driver", driver) 141 return false 142 } 143 144 return driverInfo.Detected && driverInfo.Healthy 145 } 146 147 value, ok := option.Attributes[driverStr] 148 if !ok { 149 return false 150 } 151 152 enabled, err := strconv.ParseBool(value) 153 if err != nil { 154 c.ctx.Logger().Named("driver_checker").Warn("node has invalid driver setting", "node_id", option.ID, "driver", driver, "val", value) 155 return false 156 } 157 158 if !enabled { 159 return false 160 } 161 } 162 return true 163 } 164 165 // DistinctHostsIterator is a FeasibleIterator which returns nodes that pass the 166 // distinct_hosts constraint. The constraint ensures that multiple allocations 167 // do not exist on the same node. 168 type DistinctHostsIterator struct { 169 ctx Context 170 source FeasibleIterator 171 tg *structs.TaskGroup 172 job *structs.Job 173 174 // Store whether the Job or TaskGroup has a distinct_hosts constraints so 175 // they don't have to be calculated every time Next() is called. 176 tgDistinctHosts bool 177 jobDistinctHosts bool 178 } 179 180 // NewDistinctHostsIterator creates a DistinctHostsIterator from a source. 181 func NewDistinctHostsIterator(ctx Context, source FeasibleIterator) *DistinctHostsIterator { 182 return &DistinctHostsIterator{ 183 ctx: ctx, 184 source: source, 185 } 186 } 187 188 func (iter *DistinctHostsIterator) SetTaskGroup(tg *structs.TaskGroup) { 189 iter.tg = tg 190 iter.tgDistinctHosts = iter.hasDistinctHostsConstraint(tg.Constraints) 191 } 192 193 func (iter *DistinctHostsIterator) SetJob(job *structs.Job) { 194 iter.job = job 195 iter.jobDistinctHosts = iter.hasDistinctHostsConstraint(job.Constraints) 196 } 197 198 func (iter *DistinctHostsIterator) hasDistinctHostsConstraint(constraints []*structs.Constraint) bool { 199 for _, con := range constraints { 200 if con.Operand == structs.ConstraintDistinctHosts { 201 return true 202 } 203 } 204 205 return false 206 } 207 208 func (iter *DistinctHostsIterator) Next() *structs.Node { 209 for { 210 // Get the next option from the source 211 option := iter.source.Next() 212 213 // Hot-path if the option is nil or there are no distinct_hosts or 214 // distinct_property constraints. 215 hosts := iter.jobDistinctHosts || iter.tgDistinctHosts 216 if option == nil || !hosts { 217 return option 218 } 219 220 // Check if the host constraints are satisfied 221 if !iter.satisfiesDistinctHosts(option) { 222 iter.ctx.Metrics().FilterNode(option, structs.ConstraintDistinctHosts) 223 continue 224 } 225 226 return option 227 } 228 } 229 230 // satisfiesDistinctHosts checks if the node satisfies a distinct_hosts 231 // constraint either specified at the job level or the TaskGroup level. 232 func (iter *DistinctHostsIterator) satisfiesDistinctHosts(option *structs.Node) bool { 233 // Check if there is no constraint set. 234 if !(iter.jobDistinctHosts || iter.tgDistinctHosts) { 235 return true 236 } 237 238 // Get the proposed allocations 239 proposed, err := iter.ctx.ProposedAllocs(option.ID) 240 if err != nil { 241 iter.ctx.Logger().Named("distinct_hosts").Error("failed to get proposed allocations", "error", err) 242 return false 243 } 244 245 // Skip the node if the task group has already been allocated on it. 246 for _, alloc := range proposed { 247 // If the job has a distinct_hosts constraint we only need an alloc 248 // collision on the JobID but if the constraint is on the TaskGroup then 249 // we need both a job and TaskGroup collision. 250 jobCollision := alloc.JobID == iter.job.ID 251 taskCollision := alloc.TaskGroup == iter.tg.Name 252 if iter.jobDistinctHosts && jobCollision || jobCollision && taskCollision { 253 return false 254 } 255 } 256 257 return true 258 } 259 260 func (iter *DistinctHostsIterator) Reset() { 261 iter.source.Reset() 262 } 263 264 // DistinctPropertyIterator is a FeasibleIterator which returns nodes that pass the 265 // distinct_property constraint. The constraint ensures that multiple allocations 266 // do not use the same value of the given property. 267 type DistinctPropertyIterator struct { 268 ctx Context 269 source FeasibleIterator 270 tg *structs.TaskGroup 271 job *structs.Job 272 273 hasDistinctPropertyConstraints bool 274 jobPropertySets []*propertySet 275 groupPropertySets map[string][]*propertySet 276 } 277 278 // NewDistinctPropertyIterator creates a DistinctPropertyIterator from a source. 279 func NewDistinctPropertyIterator(ctx Context, source FeasibleIterator) *DistinctPropertyIterator { 280 return &DistinctPropertyIterator{ 281 ctx: ctx, 282 source: source, 283 groupPropertySets: make(map[string][]*propertySet), 284 } 285 } 286 287 func (iter *DistinctPropertyIterator) SetTaskGroup(tg *structs.TaskGroup) { 288 iter.tg = tg 289 290 // Build the property set at the taskgroup level 291 if _, ok := iter.groupPropertySets[tg.Name]; !ok { 292 for _, c := range tg.Constraints { 293 if c.Operand != structs.ConstraintDistinctProperty { 294 continue 295 } 296 297 pset := NewPropertySet(iter.ctx, iter.job) 298 pset.SetTGConstraint(c, tg.Name) 299 iter.groupPropertySets[tg.Name] = append(iter.groupPropertySets[tg.Name], pset) 300 } 301 } 302 303 // Check if there is a distinct property 304 iter.hasDistinctPropertyConstraints = len(iter.jobPropertySets) != 0 || len(iter.groupPropertySets[tg.Name]) != 0 305 } 306 307 func (iter *DistinctPropertyIterator) SetJob(job *structs.Job) { 308 iter.job = job 309 310 // Build the property set at the job level 311 for _, c := range job.Constraints { 312 if c.Operand != structs.ConstraintDistinctProperty { 313 continue 314 } 315 316 pset := NewPropertySet(iter.ctx, job) 317 pset.SetJobConstraint(c) 318 iter.jobPropertySets = append(iter.jobPropertySets, pset) 319 } 320 } 321 322 func (iter *DistinctPropertyIterator) Next() *structs.Node { 323 for { 324 // Get the next option from the source 325 option := iter.source.Next() 326 327 // Hot path if there is nothing to check 328 if option == nil || !iter.hasDistinctPropertyConstraints { 329 return option 330 } 331 332 // Check if the constraints are met 333 if !iter.satisfiesProperties(option, iter.jobPropertySets) || 334 !iter.satisfiesProperties(option, iter.groupPropertySets[iter.tg.Name]) { 335 continue 336 } 337 338 return option 339 } 340 } 341 342 // satisfiesProperties returns whether the option satisfies the set of 343 // properties. If not it will be filtered. 344 func (iter *DistinctPropertyIterator) satisfiesProperties(option *structs.Node, set []*propertySet) bool { 345 for _, ps := range set { 346 if satisfies, reason := ps.SatisfiesDistinctProperties(option, iter.tg.Name); !satisfies { 347 iter.ctx.Metrics().FilterNode(option, reason) 348 return false 349 } 350 } 351 352 return true 353 } 354 355 func (iter *DistinctPropertyIterator) Reset() { 356 iter.source.Reset() 357 358 for _, ps := range iter.jobPropertySets { 359 ps.PopulateProposed() 360 } 361 362 for _, sets := range iter.groupPropertySets { 363 for _, ps := range sets { 364 ps.PopulateProposed() 365 } 366 } 367 } 368 369 // ConstraintChecker is a FeasibilityChecker which returns nodes that match a 370 // given set of constraints. This is used to filter on job, task group, and task 371 // constraints. 372 type ConstraintChecker struct { 373 ctx Context 374 constraints []*structs.Constraint 375 } 376 377 // NewConstraintChecker creates a ConstraintChecker for a set of constraints 378 func NewConstraintChecker(ctx Context, constraints []*structs.Constraint) *ConstraintChecker { 379 return &ConstraintChecker{ 380 ctx: ctx, 381 constraints: constraints, 382 } 383 } 384 385 func (c *ConstraintChecker) SetConstraints(constraints []*structs.Constraint) { 386 c.constraints = constraints 387 } 388 389 func (c *ConstraintChecker) Feasible(option *structs.Node) bool { 390 // Use this node if possible 391 for _, constraint := range c.constraints { 392 if !c.meetsConstraint(constraint, option) { 393 c.ctx.Metrics().FilterNode(option, constraint.String()) 394 return false 395 } 396 } 397 return true 398 } 399 400 func (c *ConstraintChecker) meetsConstraint(constraint *structs.Constraint, option *structs.Node) bool { 401 // Resolve the targets. Targets that are not present are treated as `nil`. 402 // This is to allow for matching constraints where a target is not present. 403 lVal, lOk := resolveTarget(constraint.LTarget, option) 404 rVal, rOk := resolveTarget(constraint.RTarget, option) 405 406 // Check if satisfied 407 return checkConstraint(c.ctx, constraint.Operand, lVal, rVal, lOk, rOk) 408 } 409 410 // resolveTarget is used to resolve the LTarget and RTarget of a Constraint. 411 func resolveTarget(target string, node *structs.Node) (interface{}, bool) { 412 // If no prefix, this must be a literal value 413 if !strings.HasPrefix(target, "${") { 414 return target, true 415 } 416 417 // Handle the interpolations 418 switch { 419 case "${node.unique.id}" == target: 420 return node.ID, true 421 422 case "${node.datacenter}" == target: 423 return node.Datacenter, true 424 425 case "${node.unique.name}" == target: 426 return node.Name, true 427 428 case "${node.class}" == target: 429 return node.NodeClass, true 430 431 case strings.HasPrefix(target, "${attr."): 432 attr := strings.TrimSuffix(strings.TrimPrefix(target, "${attr."), "}") 433 val, ok := node.Attributes[attr] 434 return val, ok 435 436 case strings.HasPrefix(target, "${meta."): 437 meta := strings.TrimSuffix(strings.TrimPrefix(target, "${meta."), "}") 438 val, ok := node.Meta[meta] 439 return val, ok 440 441 default: 442 return nil, false 443 } 444 } 445 446 // checkConstraint checks if a constraint is satisfied. The lVal and rVal 447 // interfaces may be nil. 448 func checkConstraint(ctx Context, operand string, lVal, rVal interface{}, lFound, rFound bool) bool { 449 // Check for constraints not handled by this checker. 450 switch operand { 451 case structs.ConstraintDistinctHosts, structs.ConstraintDistinctProperty: 452 return true 453 default: 454 break 455 } 456 457 switch operand { 458 case "=", "==", "is": 459 return lFound && rFound && reflect.DeepEqual(lVal, rVal) 460 case "!=", "not": 461 return !reflect.DeepEqual(lVal, rVal) 462 case "<", "<=", ">", ">=": 463 return lFound && rFound && checkLexicalOrder(operand, lVal, rVal) 464 case structs.ConstraintAttributeIsSet: 465 return lFound 466 case structs.ConstraintAttributeIsNotSet: 467 return !lFound 468 case structs.ConstraintVersion: 469 return lFound && rFound && checkVersionMatch(ctx, lVal, rVal) 470 case structs.ConstraintRegex: 471 return lFound && rFound && checkRegexpMatch(ctx, lVal, rVal) 472 case structs.ConstraintSetContains, structs.ConstraintSetContainsAll: 473 return lFound && rFound && checkSetContainsAll(ctx, lVal, rVal) 474 case structs.ConstraintSetContainsAny: 475 return lFound && rFound && checkSetContainsAny(lVal, rVal) 476 default: 477 return false 478 } 479 } 480 481 // checkAffinity checks if a specific affinity is satisfied 482 func checkAffinity(ctx Context, operand string, lVal, rVal interface{}, lFound, rFound bool) bool { 483 return checkConstraint(ctx, operand, lVal, rVal, lFound, rFound) 484 } 485 486 // checkAttributeAffinity checks if an affinity is satisfied 487 func checkAttributeAffinity(ctx Context, operand string, lVal, rVal *psstructs.Attribute, lFound, rFound bool) bool { 488 return checkAttributeConstraint(ctx, operand, lVal, rVal, lFound, rFound) 489 } 490 491 // checkLexicalOrder is used to check for lexical ordering 492 func checkLexicalOrder(op string, lVal, rVal interface{}) bool { 493 // Ensure the values are strings 494 lStr, ok := lVal.(string) 495 if !ok { 496 return false 497 } 498 rStr, ok := rVal.(string) 499 if !ok { 500 return false 501 } 502 503 switch op { 504 case "<": 505 return lStr < rStr 506 case "<=": 507 return lStr <= rStr 508 case ">": 509 return lStr > rStr 510 case ">=": 511 return lStr >= rStr 512 default: 513 return false 514 } 515 } 516 517 // checkVersionMatch is used to compare a version on the 518 // left hand side with a set of constraints on the right hand side 519 func checkVersionMatch(ctx Context, lVal, rVal interface{}) bool { 520 // Parse the version 521 var versionStr string 522 switch v := lVal.(type) { 523 case string: 524 versionStr = v 525 case int: 526 versionStr = fmt.Sprintf("%d", v) 527 default: 528 return false 529 } 530 531 // Parse the version 532 vers, err := version.NewVersion(versionStr) 533 if err != nil { 534 return false 535 } 536 537 // Constraint must be a string 538 constraintStr, ok := rVal.(string) 539 if !ok { 540 return false 541 } 542 543 // Check the cache for a match 544 cache := ctx.VersionConstraintCache() 545 constraints := cache[constraintStr] 546 547 // Parse the constraints 548 if constraints == nil { 549 constraints, err = version.NewConstraint(constraintStr) 550 if err != nil { 551 return false 552 } 553 cache[constraintStr] = constraints 554 } 555 556 // Check the constraints against the version 557 return constraints.Check(vers) 558 } 559 560 // checkAttributeVersionMatch is used to compare a version on the 561 // left hand side with a set of constraints on the right hand side 562 func checkAttributeVersionMatch(ctx Context, lVal, rVal *psstructs.Attribute) bool { 563 // Parse the version 564 var versionStr string 565 if s, ok := lVal.GetString(); ok { 566 versionStr = s 567 } else if i, ok := lVal.GetInt(); ok { 568 versionStr = fmt.Sprintf("%d", i) 569 } else { 570 return false 571 } 572 573 // Parse the version 574 vers, err := version.NewVersion(versionStr) 575 if err != nil { 576 return false 577 } 578 579 // Constraint must be a string 580 constraintStr, ok := rVal.GetString() 581 if !ok { 582 return false 583 } 584 585 // Check the cache for a match 586 cache := ctx.VersionConstraintCache() 587 constraints := cache[constraintStr] 588 589 // Parse the constraints 590 if constraints == nil { 591 constraints, err = version.NewConstraint(constraintStr) 592 if err != nil { 593 return false 594 } 595 cache[constraintStr] = constraints 596 } 597 598 // Check the constraints against the version 599 return constraints.Check(vers) 600 } 601 602 // checkRegexpMatch is used to compare a value on the 603 // left hand side with a regexp on the right hand side 604 func checkRegexpMatch(ctx Context, lVal, rVal interface{}) bool { 605 // Ensure left-hand is string 606 lStr, ok := lVal.(string) 607 if !ok { 608 return false 609 } 610 611 // Regexp must be a string 612 regexpStr, ok := rVal.(string) 613 if !ok { 614 return false 615 } 616 617 // Check the cache 618 cache := ctx.RegexpCache() 619 re := cache[regexpStr] 620 621 // Parse the regexp 622 if re == nil { 623 var err error 624 re, err = regexp.Compile(regexpStr) 625 if err != nil { 626 return false 627 } 628 cache[regexpStr] = re 629 } 630 631 // Look for a match 632 return re.MatchString(lStr) 633 } 634 635 // checkSetContainsAll is used to see if the left hand side contains the 636 // string on the right hand side 637 func checkSetContainsAll(ctx Context, lVal, rVal interface{}) bool { 638 // Ensure left-hand is string 639 lStr, ok := lVal.(string) 640 if !ok { 641 return false 642 } 643 644 // Regexp must be a string 645 rStr, ok := rVal.(string) 646 if !ok { 647 return false 648 } 649 650 input := strings.Split(lStr, ",") 651 lookup := make(map[string]struct{}, len(input)) 652 for _, in := range input { 653 cleaned := strings.TrimSpace(in) 654 lookup[cleaned] = struct{}{} 655 } 656 657 for _, r := range strings.Split(rStr, ",") { 658 cleaned := strings.TrimSpace(r) 659 if _, ok := lookup[cleaned]; !ok { 660 return false 661 } 662 } 663 664 return true 665 } 666 667 // checkSetContainsAny is used to see if the left hand side contains any 668 // values on the right hand side 669 func checkSetContainsAny(lVal, rVal interface{}) bool { 670 // Ensure left-hand is string 671 lStr, ok := lVal.(string) 672 if !ok { 673 return false 674 } 675 676 // RHS must be a string 677 rStr, ok := rVal.(string) 678 if !ok { 679 return false 680 } 681 682 input := strings.Split(lStr, ",") 683 lookup := make(map[string]struct{}, len(input)) 684 for _, in := range input { 685 cleaned := strings.TrimSpace(in) 686 lookup[cleaned] = struct{}{} 687 } 688 689 for _, r := range strings.Split(rStr, ",") { 690 cleaned := strings.TrimSpace(r) 691 if _, ok := lookup[cleaned]; ok { 692 return true 693 } 694 } 695 696 return false 697 } 698 699 // FeasibilityWrapper is a FeasibleIterator which wraps both job and task group 700 // FeasibilityCheckers in which feasibility checking can be skipped if the 701 // computed node class has previously been marked as eligible or ineligible. 702 type FeasibilityWrapper struct { 703 ctx Context 704 source FeasibleIterator 705 jobCheckers []FeasibilityChecker 706 tgCheckers []FeasibilityChecker 707 tg string 708 } 709 710 // NewFeasibilityWrapper returns a FeasibleIterator based on the passed source 711 // and FeasibilityCheckers. 712 func NewFeasibilityWrapper(ctx Context, source FeasibleIterator, 713 jobCheckers, tgCheckers []FeasibilityChecker) *FeasibilityWrapper { 714 return &FeasibilityWrapper{ 715 ctx: ctx, 716 source: source, 717 jobCheckers: jobCheckers, 718 tgCheckers: tgCheckers, 719 } 720 } 721 722 func (w *FeasibilityWrapper) SetTaskGroup(tg string) { 723 w.tg = tg 724 } 725 726 func (w *FeasibilityWrapper) Reset() { 727 w.source.Reset() 728 } 729 730 // Next returns an eligible node, only running the FeasibilityCheckers as needed 731 // based on the sources computed node class. 732 func (w *FeasibilityWrapper) Next() *structs.Node { 733 evalElig := w.ctx.Eligibility() 734 metrics := w.ctx.Metrics() 735 736 OUTER: 737 for { 738 // Get the next option from the source 739 option := w.source.Next() 740 if option == nil { 741 return nil 742 } 743 744 // Check if the job has been marked as eligible or ineligible. 745 jobEscaped, jobUnknown := false, false 746 switch evalElig.JobStatus(option.ComputedClass) { 747 case EvalComputedClassIneligible: 748 // Fast path the ineligible case 749 metrics.FilterNode(option, "computed class ineligible") 750 continue 751 case EvalComputedClassEscaped: 752 jobEscaped = true 753 case EvalComputedClassUnknown: 754 jobUnknown = true 755 } 756 757 // Run the job feasibility checks. 758 for _, check := range w.jobCheckers { 759 feasible := check.Feasible(option) 760 if !feasible { 761 // If the job hasn't escaped, set it to be ineligible since it 762 // failed a job check. 763 if !jobEscaped { 764 evalElig.SetJobEligibility(false, option.ComputedClass) 765 } 766 continue OUTER 767 } 768 } 769 770 // Set the job eligibility if the constraints weren't escaped and it 771 // hasn't been set before. 772 if !jobEscaped && jobUnknown { 773 evalElig.SetJobEligibility(true, option.ComputedClass) 774 } 775 776 // Check if the task group has been marked as eligible or ineligible. 777 tgEscaped, tgUnknown := false, false 778 switch evalElig.TaskGroupStatus(w.tg, option.ComputedClass) { 779 case EvalComputedClassIneligible: 780 // Fast path the ineligible case 781 metrics.FilterNode(option, "computed class ineligible") 782 continue 783 case EvalComputedClassEligible: 784 // Fast path the eligible case 785 return option 786 case EvalComputedClassEscaped: 787 tgEscaped = true 788 case EvalComputedClassUnknown: 789 tgUnknown = true 790 } 791 792 // Run the task group feasibility checks. 793 for _, check := range w.tgCheckers { 794 feasible := check.Feasible(option) 795 if !feasible { 796 // If the task group hasn't escaped, set it to be ineligible 797 // since it failed a check. 798 if !tgEscaped { 799 evalElig.SetTaskGroupEligibility(false, w.tg, option.ComputedClass) 800 } 801 continue OUTER 802 } 803 } 804 805 // Set the task group eligibility if the constraints weren't escaped and 806 // it hasn't been set before. 807 if !tgEscaped && tgUnknown { 808 evalElig.SetTaskGroupEligibility(true, w.tg, option.ComputedClass) 809 } 810 811 return option 812 } 813 } 814 815 // DeviceChecker is a FeasibilityChecker which returns whether a node has the 816 // devices necessary to scheduler a task group. 817 type DeviceChecker struct { 818 ctx Context 819 820 // required is the set of requested devices that must exist on the node 821 required []*structs.RequestedDevice 822 823 // requiresDevices indicates if the task group requires devices 824 requiresDevices bool 825 } 826 827 // NewDeviceChecker creates a DeviceChecker 828 func NewDeviceChecker(ctx Context) *DeviceChecker { 829 return &DeviceChecker{ 830 ctx: ctx, 831 } 832 } 833 834 func (c *DeviceChecker) SetTaskGroup(tg *structs.TaskGroup) { 835 c.required = nil 836 for _, task := range tg.Tasks { 837 c.required = append(c.required, task.Resources.Devices...) 838 } 839 c.requiresDevices = len(c.required) != 0 840 } 841 842 func (c *DeviceChecker) Feasible(option *structs.Node) bool { 843 if c.hasDevices(option) { 844 return true 845 } 846 847 c.ctx.Metrics().FilterNode(option, "missing devices") 848 return false 849 } 850 851 func (c *DeviceChecker) hasDevices(option *structs.Node) bool { 852 if !c.requiresDevices { 853 return true 854 } 855 856 // COMPAT(0.11): Remove in 0.11 857 // The node does not have the new resources object so it can not have any 858 // devices 859 if option.NodeResources == nil { 860 return false 861 } 862 863 // Check if the node has any devices 864 nodeDevs := option.NodeResources.Devices 865 if len(nodeDevs) == 0 { 866 return false 867 } 868 869 // Create a mapping of node devices to the remaining count 870 available := make(map[*structs.NodeDeviceResource]uint64, len(nodeDevs)) 871 for _, d := range nodeDevs { 872 var healthy uint64 = 0 873 for _, instance := range d.Instances { 874 if instance.Healthy { 875 healthy++ 876 } 877 } 878 if healthy != 0 { 879 available[d] = healthy 880 } 881 } 882 883 // Go through the required devices trying to find matches 884 OUTER: 885 for _, req := range c.required { 886 // Determine how many there are to place 887 desiredCount := req.Count 888 889 // Go through the device resources and see if we have a match 890 for d, unused := range available { 891 if unused == 0 { 892 // Depleted 893 continue 894 } 895 896 // First check we have enough instances of the device since this is 897 // cheaper than checking the constraints 898 if unused < desiredCount { 899 continue 900 } 901 902 // Check the constraints 903 if nodeDeviceMatches(c.ctx, d, req) { 904 // Consume the instances 905 available[d] -= desiredCount 906 907 // Move on to the next request 908 continue OUTER 909 } 910 } 911 912 // We couldn't match the request for the device 913 return false 914 } 915 916 // Only satisfied if there are no more devices to place 917 return true 918 } 919 920 // nodeDeviceMatches checks if the device matches the request and its 921 // constraints. It doesn't check the count. 922 func nodeDeviceMatches(ctx Context, d *structs.NodeDeviceResource, req *structs.RequestedDevice) bool { 923 if !d.ID().Matches(req.ID()) { 924 return false 925 } 926 927 // There are no constraints to consider 928 if len(req.Constraints) == 0 { 929 return true 930 } 931 932 for _, c := range req.Constraints { 933 // Resolve the targets 934 lVal, lOk := resolveDeviceTarget(c.LTarget, d) 935 rVal, rOk := resolveDeviceTarget(c.RTarget, d) 936 937 // Check if satisfied 938 if !checkAttributeConstraint(ctx, c.Operand, lVal, rVal, lOk, rOk) { 939 return false 940 } 941 } 942 943 return true 944 } 945 946 // resolveDeviceTarget is used to resolve the LTarget and RTarget of a Constraint 947 // when used on a device 948 func resolveDeviceTarget(target string, d *structs.NodeDeviceResource) (*psstructs.Attribute, bool) { 949 // If no prefix, this must be a literal value 950 if !strings.HasPrefix(target, "${") { 951 return psstructs.ParseAttribute(target), true 952 } 953 954 // Handle the interpolations 955 switch { 956 case "${device.model}" == target: 957 return psstructs.NewStringAttribute(d.Name), true 958 959 case "${device.vendor}" == target: 960 return psstructs.NewStringAttribute(d.Vendor), true 961 962 case "${device.type}" == target: 963 return psstructs.NewStringAttribute(d.Type), true 964 965 case strings.HasPrefix(target, "${device.attr."): 966 attr := strings.TrimPrefix(target, "${device.attr.") 967 attr = strings.TrimSuffix(attr, "}") 968 val, ok := d.Attributes[attr] 969 return val, ok 970 971 default: 972 return nil, false 973 } 974 } 975 976 // checkAttributeConstraint checks if a constraint is satisfied. nil equality 977 // comparisons are considered to be false. 978 func checkAttributeConstraint(ctx Context, operand string, lVal, rVal *psstructs.Attribute, lFound, rFound bool) bool { 979 // Check for constraints not handled by this checker. 980 switch operand { 981 case structs.ConstraintDistinctHosts, structs.ConstraintDistinctProperty: 982 return true 983 default: 984 break 985 } 986 987 switch operand { 988 case "!=", "not": 989 // Neither value was provided, nil != nil == false 990 if !(lFound || rFound) { 991 return false 992 } 993 994 // Only 1 value was provided, therefore nil != some == true 995 if lFound != rFound { 996 return true 997 } 998 999 // Both values were provided, so actually compare them 1000 v, ok := lVal.Compare(rVal) 1001 if !ok { 1002 return false 1003 } 1004 1005 return v != 0 1006 1007 case "<", "<=", ">", ">=", "=", "==", "is": 1008 if !(lFound && rFound) { 1009 return false 1010 } 1011 1012 v, ok := lVal.Compare(rVal) 1013 if !ok { 1014 return false 1015 } 1016 1017 switch operand { 1018 case "is", "==", "=": 1019 return v == 0 1020 case "<": 1021 return v == -1 1022 case "<=": 1023 return v != 1 1024 case ">": 1025 return v == 1 1026 case ">=": 1027 return v != -1 1028 default: 1029 return false 1030 } 1031 1032 case structs.ConstraintVersion: 1033 if !(lFound && rFound) { 1034 return false 1035 } 1036 1037 return checkAttributeVersionMatch(ctx, lVal, rVal) 1038 case structs.ConstraintRegex: 1039 if !(lFound && rFound) { 1040 return false 1041 } 1042 1043 ls, ok := lVal.GetString() 1044 rs, ok2 := rVal.GetString() 1045 if !ok || !ok2 { 1046 return false 1047 } 1048 return checkRegexpMatch(ctx, ls, rs) 1049 case structs.ConstraintSetContains, structs.ConstraintSetContainsAll: 1050 if !(lFound && rFound) { 1051 return false 1052 } 1053 1054 ls, ok := lVal.GetString() 1055 rs, ok2 := rVal.GetString() 1056 if !ok || !ok2 { 1057 return false 1058 } 1059 1060 return checkSetContainsAll(ctx, ls, rs) 1061 case structs.ConstraintSetContainsAny: 1062 if !(lFound && rFound) { 1063 return false 1064 } 1065 1066 ls, ok := lVal.GetString() 1067 rs, ok2 := rVal.GetString() 1068 if !ok || !ok2 { 1069 return false 1070 } 1071 1072 return checkSetContainsAny(ls, rs) 1073 case structs.ConstraintAttributeIsSet: 1074 return lFound 1075 case structs.ConstraintAttributeIsNotSet: 1076 return !lFound 1077 default: 1078 return false 1079 } 1080 1081 }