github.com/jrxfive/nomad@v0.6.1-0.20170802162750-1fef470e89bf/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 }