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 }