github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/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 op: structs.ConstraintSetContains, 309 lVal: "foo,bar,baz", rVal: "foo, bar ", 310 result: true, 311 }, 312 { 313 op: structs.ConstraintSetContains, 314 lVal: "foo,bar,baz", rVal: "foo,bam", 315 result: false, 316 }, 317 } 318 319 for _, tc := range cases { 320 _, ctx := testContext(t) 321 if res := checkConstraint(ctx, tc.op, tc.lVal, tc.rVal); res != tc.result { 322 t.Fatalf("TC: %#v, Result: %v", tc, res) 323 } 324 } 325 } 326 327 func TestCheckLexicalOrder(t *testing.T) { 328 type tcase struct { 329 op string 330 lVal, rVal interface{} 331 result bool 332 } 333 cases := []tcase{ 334 { 335 op: "<", 336 lVal: "bar", rVal: "foo", 337 result: true, 338 }, 339 { 340 op: "<=", 341 lVal: "foo", rVal: "foo", 342 result: true, 343 }, 344 { 345 op: ">", 346 lVal: "bar", rVal: "foo", 347 result: false, 348 }, 349 { 350 op: ">=", 351 lVal: "bar", rVal: "bar", 352 result: true, 353 }, 354 { 355 op: ">", 356 lVal: 1, rVal: "foo", 357 result: false, 358 }, 359 } 360 for _, tc := range cases { 361 if res := checkLexicalOrder(tc.op, tc.lVal, tc.rVal); res != tc.result { 362 t.Fatalf("TC: %#v, Result: %v", tc, res) 363 } 364 } 365 } 366 367 func TestCheckVersionConstraint(t *testing.T) { 368 type tcase struct { 369 lVal, rVal interface{} 370 result bool 371 } 372 cases := []tcase{ 373 { 374 lVal: "1.2.3", rVal: "~> 1.0", 375 result: true, 376 }, 377 { 378 lVal: "1.2.3", rVal: ">= 1.0, < 1.4", 379 result: true, 380 }, 381 { 382 lVal: "2.0.1", rVal: "~> 1.0", 383 result: false, 384 }, 385 { 386 lVal: "1.4", rVal: ">= 1.0, < 1.4", 387 result: false, 388 }, 389 { 390 lVal: 1, rVal: "~> 1.0", 391 result: true, 392 }, 393 } 394 for _, tc := range cases { 395 _, ctx := testContext(t) 396 if res := checkVersionConstraint(ctx, tc.lVal, tc.rVal); res != tc.result { 397 t.Fatalf("TC: %#v, Result: %v", tc, res) 398 } 399 } 400 } 401 402 func TestCheckRegexpConstraint(t *testing.T) { 403 type tcase struct { 404 lVal, rVal interface{} 405 result bool 406 } 407 cases := []tcase{ 408 { 409 lVal: "foobar", rVal: "bar", 410 result: true, 411 }, 412 { 413 lVal: "foobar", rVal: "^foo", 414 result: true, 415 }, 416 { 417 lVal: "foobar", rVal: "^bar", 418 result: false, 419 }, 420 { 421 lVal: "zipzap", rVal: "foo", 422 result: false, 423 }, 424 { 425 lVal: 1, rVal: "foo", 426 result: false, 427 }, 428 } 429 for _, tc := range cases { 430 _, ctx := testContext(t) 431 if res := checkRegexpConstraint(ctx, tc.lVal, tc.rVal); res != tc.result { 432 t.Fatalf("TC: %#v, Result: %v", tc, res) 433 } 434 } 435 } 436 437 func TestProposedAllocConstraint_JobDistinctHosts(t *testing.T) { 438 _, ctx := testContext(t) 439 nodes := []*structs.Node{ 440 mock.Node(), 441 mock.Node(), 442 mock.Node(), 443 mock.Node(), 444 } 445 static := NewStaticIterator(ctx, nodes) 446 447 // Create a job with a distinct_hosts constraint and two task groups. 448 tg1 := &structs.TaskGroup{Name: "bar"} 449 tg2 := &structs.TaskGroup{Name: "baz"} 450 451 job := &structs.Job{ 452 ID: "foo", 453 Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}}, 454 TaskGroups: []*structs.TaskGroup{tg1, tg2}, 455 } 456 457 propsed := NewProposedAllocConstraintIterator(ctx, static) 458 propsed.SetTaskGroup(tg1) 459 propsed.SetJob(job) 460 461 out := collectFeasible(propsed) 462 if len(out) != 4 { 463 t.Fatalf("Bad: %#v", out) 464 } 465 466 selected := make(map[string]struct{}, 4) 467 for _, option := range out { 468 if _, ok := selected[option.ID]; ok { 469 t.Fatalf("selected node %v for more than one alloc", option) 470 } 471 selected[option.ID] = struct{}{} 472 } 473 } 474 475 func TestProposedAllocConstraint_JobDistinctHosts_Infeasible(t *testing.T) { 476 _, ctx := testContext(t) 477 nodes := []*structs.Node{ 478 mock.Node(), 479 mock.Node(), 480 } 481 static := NewStaticIterator(ctx, nodes) 482 483 // Create a job with a distinct_hosts constraint and two task groups. 484 tg1 := &structs.TaskGroup{Name: "bar"} 485 tg2 := &structs.TaskGroup{Name: "baz"} 486 487 job := &structs.Job{ 488 ID: "foo", 489 Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}}, 490 TaskGroups: []*structs.TaskGroup{tg1, tg2}, 491 } 492 493 // Add allocs placing tg1 on node1 and tg2 on node2. This should make the 494 // job unsatisfiable. 495 plan := ctx.Plan() 496 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 497 &structs.Allocation{ 498 TaskGroup: tg1.Name, 499 JobID: job.ID, 500 ID: structs.GenerateUUID(), 501 }, 502 503 // Should be ignored as it is a different job. 504 &structs.Allocation{ 505 TaskGroup: tg2.Name, 506 JobID: "ignore 2", 507 ID: structs.GenerateUUID(), 508 }, 509 } 510 plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{ 511 &structs.Allocation{ 512 TaskGroup: tg2.Name, 513 JobID: job.ID, 514 ID: structs.GenerateUUID(), 515 }, 516 517 // Should be ignored as it is a different job. 518 &structs.Allocation{ 519 TaskGroup: tg1.Name, 520 JobID: "ignore 2", 521 ID: structs.GenerateUUID(), 522 }, 523 } 524 525 propsed := NewProposedAllocConstraintIterator(ctx, static) 526 propsed.SetTaskGroup(tg1) 527 propsed.SetJob(job) 528 529 out := collectFeasible(propsed) 530 if len(out) != 0 { 531 t.Fatalf("Bad: %#v", out) 532 } 533 } 534 535 func TestProposedAllocConstraint_JobDistinctHosts_InfeasibleCount(t *testing.T) { 536 _, ctx := testContext(t) 537 nodes := []*structs.Node{ 538 mock.Node(), 539 mock.Node(), 540 } 541 static := NewStaticIterator(ctx, nodes) 542 543 // Create a job with a distinct_hosts constraint and three task groups. 544 tg1 := &structs.TaskGroup{Name: "bar"} 545 tg2 := &structs.TaskGroup{Name: "baz"} 546 tg3 := &structs.TaskGroup{Name: "bam"} 547 548 job := &structs.Job{ 549 ID: "foo", 550 Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}}, 551 TaskGroups: []*structs.TaskGroup{tg1, tg2, tg3}, 552 } 553 554 propsed := NewProposedAllocConstraintIterator(ctx, static) 555 propsed.SetTaskGroup(tg1) 556 propsed.SetJob(job) 557 558 // It should not be able to place 3 tasks with only two nodes. 559 out := collectFeasible(propsed) 560 if len(out) != 2 { 561 t.Fatalf("Bad: %#v", out) 562 } 563 } 564 565 func TestProposedAllocConstraint_TaskGroupDistinctHosts(t *testing.T) { 566 _, ctx := testContext(t) 567 nodes := []*structs.Node{ 568 mock.Node(), 569 mock.Node(), 570 } 571 static := NewStaticIterator(ctx, nodes) 572 573 // Create a task group with a distinct_hosts constraint. 574 taskGroup := &structs.TaskGroup{ 575 Name: "example", 576 Constraints: []*structs.Constraint{ 577 {Operand: structs.ConstraintDistinctHosts}, 578 }, 579 } 580 581 // Add a planned alloc to node1. 582 plan := ctx.Plan() 583 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 584 &structs.Allocation{ 585 TaskGroup: taskGroup.Name, 586 JobID: "foo", 587 }, 588 } 589 590 // Add a planned alloc to node2 with the same task group name but a 591 // different job. 592 plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{ 593 &structs.Allocation{ 594 TaskGroup: taskGroup.Name, 595 JobID: "bar", 596 }, 597 } 598 599 propsed := NewProposedAllocConstraintIterator(ctx, static) 600 propsed.SetTaskGroup(taskGroup) 601 propsed.SetJob(&structs.Job{ID: "foo"}) 602 603 out := collectFeasible(propsed) 604 if len(out) != 1 { 605 t.Fatalf("Bad: %#v", out) 606 } 607 608 // Expect it to skip the first node as there is a previous alloc on it for 609 // the same task group. 610 if out[0] != nodes[1] { 611 t.Fatalf("Bad: %v", out) 612 } 613 } 614 615 func collectFeasible(iter FeasibleIterator) (out []*structs.Node) { 616 for { 617 next := iter.Next() 618 if next == nil { 619 break 620 } 621 out = append(out, next) 622 } 623 return 624 } 625 626 // mockFeasibilityChecker is a FeasibilityChecker that returns predetermined 627 // feasibility values. 628 type mockFeasibilityChecker struct { 629 retVals []bool 630 i int 631 } 632 633 func newMockFeasiblityChecker(values ...bool) *mockFeasibilityChecker { 634 return &mockFeasibilityChecker{retVals: values} 635 } 636 637 func (c *mockFeasibilityChecker) Feasible(*structs.Node) bool { 638 if c.i >= len(c.retVals) { 639 c.i++ 640 return false 641 } 642 643 f := c.retVals[c.i] 644 c.i++ 645 return f 646 } 647 648 // calls returns how many times the checker was called. 649 func (c *mockFeasibilityChecker) calls() int { return c.i } 650 651 func TestFeasibilityWrapper_JobIneligible(t *testing.T) { 652 _, ctx := testContext(t) 653 nodes := []*structs.Node{mock.Node()} 654 static := NewStaticIterator(ctx, nodes) 655 mocked := newMockFeasiblityChecker(false) 656 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil) 657 658 // Set the job to ineligible 659 ctx.Eligibility().SetJobEligibility(false, nodes[0].ComputedClass) 660 661 // Run the wrapper. 662 out := collectFeasible(wrapper) 663 664 if out != nil || mocked.calls() != 0 { 665 t.Fatalf("bad: %#v %d", out, mocked.calls()) 666 } 667 } 668 669 func TestFeasibilityWrapper_JobEscapes(t *testing.T) { 670 _, ctx := testContext(t) 671 nodes := []*structs.Node{mock.Node()} 672 static := NewStaticIterator(ctx, nodes) 673 mocked := newMockFeasiblityChecker(false) 674 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil) 675 676 // Set the job to escaped 677 cc := nodes[0].ComputedClass 678 ctx.Eligibility().job[cc] = EvalComputedClassEscaped 679 680 // Run the wrapper. 681 out := collectFeasible(wrapper) 682 683 if out != nil || mocked.calls() != 1 { 684 t.Fatalf("bad: %#v", out) 685 } 686 687 // Ensure that the job status didn't change from escaped even though the 688 // option failed. 689 if status := ctx.Eligibility().JobStatus(cc); status != EvalComputedClassEscaped { 690 t.Fatalf("job status is %v; want %v", status, EvalComputedClassEscaped) 691 } 692 } 693 694 func TestFeasibilityWrapper_JobAndTg_Eligible(t *testing.T) { 695 _, ctx := testContext(t) 696 nodes := []*structs.Node{mock.Node()} 697 static := NewStaticIterator(ctx, nodes) 698 jobMock := newMockFeasiblityChecker(true) 699 tgMock := newMockFeasiblityChecker(false) 700 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 701 702 // Set the job to escaped 703 cc := nodes[0].ComputedClass 704 ctx.Eligibility().job[cc] = EvalComputedClassEligible 705 ctx.Eligibility().SetTaskGroupEligibility(true, "foo", cc) 706 wrapper.SetTaskGroup("foo") 707 708 // Run the wrapper. 709 out := collectFeasible(wrapper) 710 711 if out == nil || tgMock.calls() != 0 { 712 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 713 } 714 } 715 716 func TestFeasibilityWrapper_JobEligible_TgIneligible(t *testing.T) { 717 _, ctx := testContext(t) 718 nodes := []*structs.Node{mock.Node()} 719 static := NewStaticIterator(ctx, nodes) 720 jobMock := newMockFeasiblityChecker(true) 721 tgMock := newMockFeasiblityChecker(false) 722 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 723 724 // Set the job to escaped 725 cc := nodes[0].ComputedClass 726 ctx.Eligibility().job[cc] = EvalComputedClassEligible 727 ctx.Eligibility().SetTaskGroupEligibility(false, "foo", cc) 728 wrapper.SetTaskGroup("foo") 729 730 // Run the wrapper. 731 out := collectFeasible(wrapper) 732 733 if out != nil || tgMock.calls() != 0 { 734 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 735 } 736 } 737 738 func TestFeasibilityWrapper_JobEligible_TgEscaped(t *testing.T) { 739 _, ctx := testContext(t) 740 nodes := []*structs.Node{mock.Node()} 741 static := NewStaticIterator(ctx, nodes) 742 jobMock := newMockFeasiblityChecker(true) 743 tgMock := newMockFeasiblityChecker(true) 744 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 745 746 // Set the job to escaped 747 cc := nodes[0].ComputedClass 748 ctx.Eligibility().job[cc] = EvalComputedClassEligible 749 ctx.Eligibility().taskGroups["foo"] = 750 map[string]ComputedClassFeasibility{cc: EvalComputedClassEscaped} 751 wrapper.SetTaskGroup("foo") 752 753 // Run the wrapper. 754 out := collectFeasible(wrapper) 755 756 if out == nil || tgMock.calls() != 1 { 757 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 758 } 759 760 if e, ok := ctx.Eligibility().taskGroups["foo"][cc]; !ok || e != EvalComputedClassEscaped { 761 t.Fatalf("bad: %v %v", e, ok) 762 } 763 }