github.com/ranjib/nomad@v0.1.1-0.20160225204057-97751b02f70b/scheduler/feasible_test.go (about) 1 package scheduler 2 3 import ( 4 "reflect" 5 "testing" 6 7 "github.com/hashicorp/nomad/nomad/mock" 8 "github.com/hashicorp/nomad/nomad/structs" 9 ) 10 11 func TestStaticIterator_Reset(t *testing.T) { 12 _, ctx := testContext(t) 13 var nodes []*structs.Node 14 for i := 0; i < 3; i++ { 15 nodes = append(nodes, mock.Node()) 16 } 17 static := NewStaticIterator(ctx, nodes) 18 19 for i := 0; i < 6; i++ { 20 static.Reset() 21 for j := 0; j < i; j++ { 22 static.Next() 23 } 24 static.Reset() 25 26 out := collectFeasible(static) 27 if len(out) != len(nodes) { 28 t.Fatalf("out: %#v", out) 29 t.Fatalf("missing nodes %d %#v", i, static) 30 } 31 32 ids := make(map[string]struct{}) 33 for _, o := range out { 34 if _, ok := ids[o.ID]; ok { 35 t.Fatalf("duplicate") 36 } 37 ids[o.ID] = struct{}{} 38 } 39 } 40 } 41 42 func TestStaticIterator_SetNodes(t *testing.T) { 43 _, ctx := testContext(t) 44 var nodes []*structs.Node 45 for i := 0; i < 3; i++ { 46 nodes = append(nodes, mock.Node()) 47 } 48 static := NewStaticIterator(ctx, nodes) 49 50 newNodes := []*structs.Node{mock.Node()} 51 static.SetNodes(newNodes) 52 53 out := collectFeasible(static) 54 if !reflect.DeepEqual(out, newNodes) { 55 t.Fatalf("bad: %#v", out) 56 } 57 } 58 59 func TestRandomIterator(t *testing.T) { 60 _, ctx := testContext(t) 61 var nodes []*structs.Node 62 for i := 0; i < 10; i++ { 63 nodes = append(nodes, mock.Node()) 64 } 65 66 nc := make([]*structs.Node, len(nodes)) 67 copy(nc, nodes) 68 rand := NewRandomIterator(ctx, nc) 69 70 out := collectFeasible(rand) 71 if len(out) != len(nodes) { 72 t.Fatalf("missing nodes") 73 } 74 if reflect.DeepEqual(out, nodes) { 75 t.Fatalf("same order") 76 } 77 } 78 79 func TestDriverChecker(t *testing.T) { 80 _, ctx := testContext(t) 81 nodes := []*structs.Node{ 82 mock.Node(), 83 mock.Node(), 84 mock.Node(), 85 mock.Node(), 86 } 87 nodes[0].Attributes["driver.foo"] = "1" 88 nodes[1].Attributes["driver.foo"] = "0" 89 nodes[2].Attributes["driver.foo"] = "true" 90 nodes[3].Attributes["driver.foo"] = "False" 91 92 drivers := map[string]struct{}{ 93 "exec": struct{}{}, 94 "foo": struct{}{}, 95 } 96 checker := NewDriverChecker(ctx, drivers) 97 cases := []struct { 98 Node *structs.Node 99 Result bool 100 }{ 101 { 102 Node: nodes[0], 103 Result: true, 104 }, 105 { 106 Node: nodes[1], 107 Result: false, 108 }, 109 { 110 Node: nodes[2], 111 Result: true, 112 }, 113 { 114 Node: nodes[3], 115 Result: false, 116 }, 117 } 118 119 for i, c := range cases { 120 if act := checker.Feasible(c.Node); act != c.Result { 121 t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result) 122 } 123 } 124 } 125 126 func TestConstraintChecker(t *testing.T) { 127 _, ctx := testContext(t) 128 nodes := []*structs.Node{ 129 mock.Node(), 130 mock.Node(), 131 mock.Node(), 132 mock.Node(), 133 } 134 135 nodes[0].Attributes["kernel.name"] = "freebsd" 136 nodes[1].Datacenter = "dc2" 137 nodes[2].NodeClass = "large" 138 139 constraints := []*structs.Constraint{ 140 &structs.Constraint{ 141 Operand: "=", 142 LTarget: "${node.datacenter}", 143 RTarget: "dc1", 144 }, 145 &structs.Constraint{ 146 Operand: "is", 147 LTarget: "${attr.kernel.name}", 148 RTarget: "linux", 149 }, 150 &structs.Constraint{ 151 Operand: "is", 152 LTarget: "${node.class}", 153 RTarget: "large", 154 }, 155 } 156 checker := NewConstraintChecker(ctx, constraints) 157 cases := []struct { 158 Node *structs.Node 159 Result bool 160 }{ 161 { 162 Node: nodes[0], 163 Result: false, 164 }, 165 { 166 Node: nodes[1], 167 Result: false, 168 }, 169 { 170 Node: nodes[2], 171 Result: true, 172 }, 173 } 174 175 for i, c := range cases { 176 if act := checker.Feasible(c.Node); act != c.Result { 177 t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result) 178 } 179 } 180 } 181 182 func TestResolveConstraintTarget(t *testing.T) { 183 type tcase struct { 184 target string 185 node *structs.Node 186 val interface{} 187 result bool 188 } 189 node := mock.Node() 190 cases := []tcase{ 191 { 192 target: "${node.unique.id}", 193 node: node, 194 val: node.ID, 195 result: true, 196 }, 197 { 198 target: "${node.datacenter}", 199 node: node, 200 val: node.Datacenter, 201 result: true, 202 }, 203 { 204 target: "${node.unique.name}", 205 node: node, 206 val: node.Name, 207 result: true, 208 }, 209 { 210 target: "${node.class}", 211 node: node, 212 val: node.NodeClass, 213 result: true, 214 }, 215 { 216 target: "${node.foo}", 217 node: node, 218 result: false, 219 }, 220 { 221 target: "${attr.kernel.name}", 222 node: node, 223 val: node.Attributes["kernel.name"], 224 result: true, 225 }, 226 { 227 target: "${attr.rand}", 228 node: node, 229 result: false, 230 }, 231 { 232 target: "${meta.pci-dss}", 233 node: node, 234 val: node.Meta["pci-dss"], 235 result: true, 236 }, 237 { 238 target: "${meta.rand}", 239 node: node, 240 result: false, 241 }, 242 } 243 244 for _, tc := range cases { 245 res, ok := resolveConstraintTarget(tc.target, tc.node) 246 if ok != tc.result { 247 t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok) 248 } 249 if ok && !reflect.DeepEqual(res, tc.val) { 250 t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok) 251 } 252 } 253 } 254 255 func TestCheckConstraint(t *testing.T) { 256 type tcase struct { 257 op string 258 lVal, rVal interface{} 259 result bool 260 } 261 cases := []tcase{ 262 { 263 op: "=", 264 lVal: "foo", rVal: "foo", 265 result: true, 266 }, 267 { 268 op: "is", 269 lVal: "foo", rVal: "foo", 270 result: true, 271 }, 272 { 273 op: "==", 274 lVal: "foo", rVal: "foo", 275 result: true, 276 }, 277 { 278 op: "!=", 279 lVal: "foo", rVal: "foo", 280 result: false, 281 }, 282 { 283 op: "!=", 284 lVal: "foo", rVal: "bar", 285 result: true, 286 }, 287 { 288 op: "not", 289 lVal: "foo", rVal: "bar", 290 result: true, 291 }, 292 { 293 op: structs.ConstraintVersion, 294 lVal: "1.2.3", rVal: "~> 1.0", 295 result: true, 296 }, 297 { 298 op: structs.ConstraintRegex, 299 lVal: "foobarbaz", rVal: "[\\w]+", 300 result: true, 301 }, 302 { 303 op: "<", 304 lVal: "foo", rVal: "bar", 305 result: false, 306 }, 307 } 308 309 for _, tc := range cases { 310 _, ctx := testContext(t) 311 if res := checkConstraint(ctx, tc.op, tc.lVal, tc.rVal); res != tc.result { 312 t.Fatalf("TC: %#v, Result: %v", tc, res) 313 } 314 } 315 } 316 317 func TestCheckLexicalOrder(t *testing.T) { 318 type tcase struct { 319 op string 320 lVal, rVal interface{} 321 result bool 322 } 323 cases := []tcase{ 324 { 325 op: "<", 326 lVal: "bar", rVal: "foo", 327 result: true, 328 }, 329 { 330 op: "<=", 331 lVal: "foo", rVal: "foo", 332 result: true, 333 }, 334 { 335 op: ">", 336 lVal: "bar", rVal: "foo", 337 result: false, 338 }, 339 { 340 op: ">=", 341 lVal: "bar", rVal: "bar", 342 result: true, 343 }, 344 { 345 op: ">", 346 lVal: 1, rVal: "foo", 347 result: false, 348 }, 349 } 350 for _, tc := range cases { 351 if res := checkLexicalOrder(tc.op, tc.lVal, tc.rVal); res != tc.result { 352 t.Fatalf("TC: %#v, Result: %v", tc, res) 353 } 354 } 355 } 356 357 func TestCheckVersionConstraint(t *testing.T) { 358 type tcase struct { 359 lVal, rVal interface{} 360 result bool 361 } 362 cases := []tcase{ 363 { 364 lVal: "1.2.3", rVal: "~> 1.0", 365 result: true, 366 }, 367 { 368 lVal: "1.2.3", rVal: ">= 1.0, < 1.4", 369 result: true, 370 }, 371 { 372 lVal: "2.0.1", rVal: "~> 1.0", 373 result: false, 374 }, 375 { 376 lVal: "1.4", rVal: ">= 1.0, < 1.4", 377 result: false, 378 }, 379 { 380 lVal: 1, rVal: "~> 1.0", 381 result: true, 382 }, 383 } 384 for _, tc := range cases { 385 _, ctx := testContext(t) 386 if res := checkVersionConstraint(ctx, tc.lVal, tc.rVal); res != tc.result { 387 t.Fatalf("TC: %#v, Result: %v", tc, res) 388 } 389 } 390 } 391 392 func TestCheckRegexpConstraint(t *testing.T) { 393 type tcase struct { 394 lVal, rVal interface{} 395 result bool 396 } 397 cases := []tcase{ 398 { 399 lVal: "foobar", rVal: "bar", 400 result: true, 401 }, 402 { 403 lVal: "foobar", rVal: "^foo", 404 result: true, 405 }, 406 { 407 lVal: "foobar", rVal: "^bar", 408 result: false, 409 }, 410 { 411 lVal: "zipzap", rVal: "foo", 412 result: false, 413 }, 414 { 415 lVal: 1, rVal: "foo", 416 result: false, 417 }, 418 } 419 for _, tc := range cases { 420 _, ctx := testContext(t) 421 if res := checkRegexpConstraint(ctx, tc.lVal, tc.rVal); res != tc.result { 422 t.Fatalf("TC: %#v, Result: %v", tc, res) 423 } 424 } 425 } 426 427 func TestProposedAllocConstraint_JobDistinctHosts(t *testing.T) { 428 _, ctx := testContext(t) 429 nodes := []*structs.Node{ 430 mock.Node(), 431 mock.Node(), 432 mock.Node(), 433 mock.Node(), 434 } 435 static := NewStaticIterator(ctx, nodes) 436 437 // Create a job with a distinct_hosts constraint and two task groups. 438 tg1 := &structs.TaskGroup{Name: "bar"} 439 tg2 := &structs.TaskGroup{Name: "baz"} 440 441 job := &structs.Job{ 442 ID: "foo", 443 Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}}, 444 TaskGroups: []*structs.TaskGroup{tg1, tg2}, 445 } 446 447 propsed := NewProposedAllocConstraintIterator(ctx, static) 448 propsed.SetTaskGroup(tg1) 449 propsed.SetJob(job) 450 451 out := collectFeasible(propsed) 452 if len(out) != 4 { 453 t.Fatalf("Bad: %#v", out) 454 } 455 456 selected := make(map[string]struct{}, 4) 457 for _, option := range out { 458 if _, ok := selected[option.ID]; ok { 459 t.Fatalf("selected node %v for more than one alloc", option) 460 } 461 selected[option.ID] = struct{}{} 462 } 463 } 464 465 func TestProposedAllocConstraint_JobDistinctHosts_Infeasible(t *testing.T) { 466 _, ctx := testContext(t) 467 nodes := []*structs.Node{ 468 mock.Node(), 469 mock.Node(), 470 } 471 static := NewStaticIterator(ctx, nodes) 472 473 // Create a job with a distinct_hosts constraint and two task groups. 474 tg1 := &structs.TaskGroup{Name: "bar"} 475 tg2 := &structs.TaskGroup{Name: "baz"} 476 477 job := &structs.Job{ 478 ID: "foo", 479 Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}}, 480 TaskGroups: []*structs.TaskGroup{tg1, tg2}, 481 } 482 483 // Add allocs placing tg1 on node1 and tg2 on node2. This should make the 484 // job unsatisfiable. 485 plan := ctx.Plan() 486 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 487 &structs.Allocation{ 488 TaskGroup: tg1.Name, 489 JobID: job.ID, 490 }, 491 492 // Should be ignored as it is a different job. 493 &structs.Allocation{ 494 TaskGroup: tg2.Name, 495 JobID: "ignore 2", 496 }, 497 } 498 plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{ 499 &structs.Allocation{ 500 TaskGroup: tg2.Name, 501 JobID: job.ID, 502 }, 503 504 // Should be ignored as it is a different job. 505 &structs.Allocation{ 506 TaskGroup: tg1.Name, 507 JobID: "ignore 2", 508 }, 509 } 510 511 propsed := NewProposedAllocConstraintIterator(ctx, static) 512 propsed.SetTaskGroup(tg1) 513 propsed.SetJob(job) 514 515 out := collectFeasible(propsed) 516 if len(out) != 0 { 517 t.Fatalf("Bad: %#v", out) 518 } 519 } 520 521 func TestProposedAllocConstraint_JobDistinctHosts_InfeasibleCount(t *testing.T) { 522 _, ctx := testContext(t) 523 nodes := []*structs.Node{ 524 mock.Node(), 525 mock.Node(), 526 } 527 static := NewStaticIterator(ctx, nodes) 528 529 // Create a job with a distinct_hosts constraint and three task groups. 530 tg1 := &structs.TaskGroup{Name: "bar"} 531 tg2 := &structs.TaskGroup{Name: "baz"} 532 tg3 := &structs.TaskGroup{Name: "bam"} 533 534 job := &structs.Job{ 535 ID: "foo", 536 Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}}, 537 TaskGroups: []*structs.TaskGroup{tg1, tg2, tg3}, 538 } 539 540 propsed := NewProposedAllocConstraintIterator(ctx, static) 541 propsed.SetTaskGroup(tg1) 542 propsed.SetJob(job) 543 544 // It should not be able to place 3 tasks with only two nodes. 545 out := collectFeasible(propsed) 546 if len(out) != 2 { 547 t.Fatalf("Bad: %#v", out) 548 } 549 } 550 551 func TestProposedAllocConstraint_TaskGroupDistinctHosts(t *testing.T) { 552 _, ctx := testContext(t) 553 nodes := []*structs.Node{ 554 mock.Node(), 555 mock.Node(), 556 } 557 static := NewStaticIterator(ctx, nodes) 558 559 // Create a task group with a distinct_hosts constraint. 560 taskGroup := &structs.TaskGroup{ 561 Name: "example", 562 Constraints: []*structs.Constraint{ 563 {Operand: structs.ConstraintDistinctHosts}, 564 }, 565 } 566 567 // Add a planned alloc to node1. 568 plan := ctx.Plan() 569 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 570 &structs.Allocation{ 571 TaskGroup: taskGroup.Name, 572 JobID: "foo", 573 }, 574 } 575 576 // Add a planned alloc to node2 with the same task group name but a 577 // different job. 578 plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{ 579 &structs.Allocation{ 580 TaskGroup: taskGroup.Name, 581 JobID: "bar", 582 }, 583 } 584 585 propsed := NewProposedAllocConstraintIterator(ctx, static) 586 propsed.SetTaskGroup(taskGroup) 587 propsed.SetJob(&structs.Job{ID: "foo"}) 588 589 out := collectFeasible(propsed) 590 if len(out) != 1 { 591 t.Fatalf("Bad: %#v", out) 592 } 593 594 // Expect it to skip the first node as there is a previous alloc on it for 595 // the same task group. 596 if out[0] != nodes[1] { 597 t.Fatalf("Bad: %v", out) 598 } 599 } 600 601 func collectFeasible(iter FeasibleIterator) (out []*structs.Node) { 602 for { 603 next := iter.Next() 604 if next == nil { 605 break 606 } 607 out = append(out, next) 608 } 609 return 610 } 611 612 // mockFeasibilityChecker is a FeasibilityChecker that returns predetermined 613 // feasibility values. 614 type mockFeasibilityChecker struct { 615 retVals []bool 616 i int 617 } 618 619 func newMockFeasiblityChecker(values ...bool) *mockFeasibilityChecker { 620 return &mockFeasibilityChecker{retVals: values} 621 } 622 623 func (c *mockFeasibilityChecker) Feasible(*structs.Node) bool { 624 if c.i >= len(c.retVals) { 625 c.i++ 626 return false 627 } 628 629 f := c.retVals[c.i] 630 c.i++ 631 return f 632 } 633 634 // calls returns how many times the checker was called. 635 func (c *mockFeasibilityChecker) calls() int { return c.i } 636 637 func TestFeasibilityWrapper_JobIneligible(t *testing.T) { 638 _, ctx := testContext(t) 639 nodes := []*structs.Node{mock.Node()} 640 static := NewStaticIterator(ctx, nodes) 641 mocked := newMockFeasiblityChecker(false) 642 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil) 643 644 // Set the job to ineligible 645 ctx.Eligibility().SetJobEligibility(false, nodes[0].ComputedClass) 646 647 // Run the wrapper. 648 out := collectFeasible(wrapper) 649 650 if out != nil || mocked.calls() != 0 { 651 t.Fatalf("bad: %#v %d", out, mocked.calls()) 652 } 653 } 654 655 func TestFeasibilityWrapper_JobEscapes(t *testing.T) { 656 _, ctx := testContext(t) 657 nodes := []*structs.Node{mock.Node()} 658 static := NewStaticIterator(ctx, nodes) 659 mocked := newMockFeasiblityChecker(false) 660 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil) 661 662 // Set the job to escaped 663 cc := nodes[0].ComputedClass 664 ctx.Eligibility().job[cc] = EvalComputedClassEscaped 665 666 // Run the wrapper. 667 out := collectFeasible(wrapper) 668 669 if out != nil || mocked.calls() != 1 { 670 t.Fatalf("bad: %#v", out) 671 } 672 673 // Ensure that the job status didn't change from escaped even though the 674 // option failed. 675 if status := ctx.Eligibility().JobStatus(cc); status != EvalComputedClassEscaped { 676 t.Fatalf("job status is %v; want %v", status, EvalComputedClassEscaped) 677 } 678 } 679 680 func TestFeasibilityWrapper_JobAndTg_Eligible(t *testing.T) { 681 _, ctx := testContext(t) 682 nodes := []*structs.Node{mock.Node()} 683 static := NewStaticIterator(ctx, nodes) 684 jobMock := newMockFeasiblityChecker(true) 685 tgMock := newMockFeasiblityChecker(false) 686 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 687 688 // Set the job to escaped 689 cc := nodes[0].ComputedClass 690 ctx.Eligibility().job[cc] = EvalComputedClassEligible 691 ctx.Eligibility().SetTaskGroupEligibility(true, "foo", cc) 692 wrapper.SetTaskGroup("foo") 693 694 // Run the wrapper. 695 out := collectFeasible(wrapper) 696 697 if out == nil || tgMock.calls() != 0 { 698 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 699 } 700 } 701 702 func TestFeasibilityWrapper_JobEligible_TgIneligible(t *testing.T) { 703 _, ctx := testContext(t) 704 nodes := []*structs.Node{mock.Node()} 705 static := NewStaticIterator(ctx, nodes) 706 jobMock := newMockFeasiblityChecker(true) 707 tgMock := newMockFeasiblityChecker(false) 708 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 709 710 // Set the job to escaped 711 cc := nodes[0].ComputedClass 712 ctx.Eligibility().job[cc] = EvalComputedClassEligible 713 ctx.Eligibility().SetTaskGroupEligibility(false, "foo", cc) 714 wrapper.SetTaskGroup("foo") 715 716 // Run the wrapper. 717 out := collectFeasible(wrapper) 718 719 if out != nil || tgMock.calls() != 0 { 720 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 721 } 722 } 723 724 func TestFeasibilityWrapper_JobEligible_TgEscaped(t *testing.T) { 725 _, ctx := testContext(t) 726 nodes := []*structs.Node{mock.Node()} 727 static := NewStaticIterator(ctx, nodes) 728 jobMock := newMockFeasiblityChecker(true) 729 tgMock := newMockFeasiblityChecker(true) 730 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 731 732 // Set the job to escaped 733 cc := nodes[0].ComputedClass 734 ctx.Eligibility().job[cc] = EvalComputedClassEligible 735 ctx.Eligibility().taskGroups["foo"] = 736 map[string]ComputedClassFeasibility{cc: EvalComputedClassEscaped} 737 wrapper.SetTaskGroup("foo") 738 739 // Run the wrapper. 740 out := collectFeasible(wrapper) 741 742 if out == nil || tgMock.calls() != 1 { 743 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 744 } 745 746 if e, ok := ctx.Eligibility().taskGroups["foo"][cc]; !ok || e != EvalComputedClassEscaped { 747 t.Fatalf("bad: %v %v", e, ok) 748 } 749 }