github.com/dkerwin/nomad@v0.3.3-0.20160525181927-74554135514b/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 default: 348 return false 349 } 350 } 351 352 // checkLexicalOrder is used to check for lexical ordering 353 func checkLexicalOrder(op string, lVal, rVal interface{}) bool { 354 // Ensure the values are strings 355 lStr, ok := lVal.(string) 356 if !ok { 357 return false 358 } 359 rStr, ok := rVal.(string) 360 if !ok { 361 return false 362 } 363 364 switch op { 365 case "<": 366 return lStr < rStr 367 case "<=": 368 return lStr <= rStr 369 case ">": 370 return lStr > rStr 371 case ">=": 372 return lStr >= rStr 373 default: 374 return false 375 } 376 } 377 378 // checkVersionConstraint is used to compare a version on the 379 // left hand side with a set of constraints on the right hand side 380 func checkVersionConstraint(ctx Context, lVal, rVal interface{}) bool { 381 // Parse the version 382 var versionStr string 383 switch v := lVal.(type) { 384 case string: 385 versionStr = v 386 case int: 387 versionStr = fmt.Sprintf("%d", v) 388 default: 389 return false 390 } 391 392 // Parse the version 393 vers, err := version.NewVersion(versionStr) 394 if err != nil { 395 return false 396 } 397 398 // Constraint must be a string 399 constraintStr, ok := rVal.(string) 400 if !ok { 401 return false 402 } 403 404 // Check the cache for a match 405 cache := ctx.ConstraintCache() 406 constraints := cache[constraintStr] 407 408 // Parse the constraints 409 if constraints == nil { 410 constraints, err = version.NewConstraint(constraintStr) 411 if err != nil { 412 return false 413 } 414 cache[constraintStr] = constraints 415 } 416 417 // Check the constraints against the version 418 return constraints.Check(vers) 419 } 420 421 // checkRegexpConstraint is used to compare a value on the 422 // left hand side with a regexp on the right hand side 423 func checkRegexpConstraint(ctx Context, lVal, rVal interface{}) bool { 424 // Ensure left-hand is string 425 lStr, ok := lVal.(string) 426 if !ok { 427 return false 428 } 429 430 // Regexp must be a string 431 regexpStr, ok := rVal.(string) 432 if !ok { 433 return false 434 } 435 436 // Check the cache 437 cache := ctx.RegexpCache() 438 re := cache[regexpStr] 439 440 // Parse the regexp 441 if re == nil { 442 var err error 443 re, err = regexp.Compile(regexpStr) 444 if err != nil { 445 return false 446 } 447 cache[regexpStr] = re 448 } 449 450 // Look for a match 451 return re.MatchString(lStr) 452 } 453 454 // FeasibilityWrapper is a FeasibleIterator which wraps both job and task group 455 // FeasibilityCheckers in which feasibility checking can be skipped if the 456 // computed node class has previously been marked as eligible or ineligible. 457 type FeasibilityWrapper struct { 458 ctx Context 459 source FeasibleIterator 460 jobCheckers []FeasibilityChecker 461 tgCheckers []FeasibilityChecker 462 tg string 463 } 464 465 // NewFeasibilityWrapper returns a FeasibleIterator based on the passed source 466 // and FeasibilityCheckers. 467 func NewFeasibilityWrapper(ctx Context, source FeasibleIterator, 468 jobCheckers, tgCheckers []FeasibilityChecker) *FeasibilityWrapper { 469 return &FeasibilityWrapper{ 470 ctx: ctx, 471 source: source, 472 jobCheckers: jobCheckers, 473 tgCheckers: tgCheckers, 474 } 475 } 476 477 func (w *FeasibilityWrapper) SetTaskGroup(tg string) { 478 w.tg = tg 479 } 480 481 func (w *FeasibilityWrapper) Reset() { 482 w.source.Reset() 483 } 484 485 // Next returns an eligible node, only running the FeasibilityCheckers as needed 486 // based on the sources computed node class. 487 func (w *FeasibilityWrapper) Next() *structs.Node { 488 evalElig := w.ctx.Eligibility() 489 metrics := w.ctx.Metrics() 490 491 OUTER: 492 for { 493 // Get the next option from the source 494 option := w.source.Next() 495 if option == nil { 496 return nil 497 } 498 499 // Check if the job has been marked as eligible or ineligible. 500 jobEscaped, jobUnknown := false, false 501 switch evalElig.JobStatus(option.ComputedClass) { 502 case EvalComputedClassIneligible: 503 // Fast path the ineligible case 504 metrics.FilterNode(option, "computed class ineligible") 505 continue 506 case EvalComputedClassEscaped: 507 jobEscaped = true 508 case EvalComputedClassUnknown: 509 jobUnknown = true 510 } 511 512 // Run the job feasibility checks. 513 for _, check := range w.jobCheckers { 514 feasible := check.Feasible(option) 515 if !feasible { 516 // If the job hasn't escaped, set it to be ineligible since it 517 // failed a job check. 518 if !jobEscaped { 519 evalElig.SetJobEligibility(false, option.ComputedClass) 520 } 521 continue OUTER 522 } 523 } 524 525 // Set the job eligibility if the constraints weren't escaped and it 526 // hasn't been set before. 527 if !jobEscaped && jobUnknown { 528 evalElig.SetJobEligibility(true, option.ComputedClass) 529 } 530 531 // Check if the task group has been marked as eligible or ineligible. 532 tgEscaped, tgUnknown := false, false 533 switch evalElig.TaskGroupStatus(w.tg, option.ComputedClass) { 534 case EvalComputedClassIneligible: 535 // Fast path the ineligible case 536 metrics.FilterNode(option, "computed class ineligible") 537 continue 538 case EvalComputedClassEligible: 539 // Fast path the eligible case 540 return option 541 case EvalComputedClassEscaped: 542 tgEscaped = true 543 case EvalComputedClassUnknown: 544 tgUnknown = true 545 } 546 547 // Run the task group feasibility checks. 548 for _, check := range w.tgCheckers { 549 feasible := check.Feasible(option) 550 if !feasible { 551 // If the task group hasn't escaped, set it to be ineligible 552 // since it failed a check. 553 if !tgEscaped { 554 evalElig.SetTaskGroupEligibility(false, w.tg, option.ComputedClass) 555 } 556 continue OUTER 557 } 558 } 559 560 // Set the task group eligibility if the constraints weren't escaped and 561 // it hasn't been set before. 562 if !tgEscaped && tgUnknown { 563 evalElig.SetTaskGroupEligibility(true, w.tg, option.ComputedClass) 564 } 565 566 return option 567 } 568 }