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