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