github.com/dkerwin/nomad@v0.3.3-0.20160525181927-74554135514b/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 ID: structs.GenerateUUID(), 491 }, 492 493 // Should be ignored as it is a different job. 494 &structs.Allocation{ 495 TaskGroup: tg2.Name, 496 JobID: "ignore 2", 497 ID: structs.GenerateUUID(), 498 }, 499 } 500 plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{ 501 &structs.Allocation{ 502 TaskGroup: tg2.Name, 503 JobID: job.ID, 504 ID: structs.GenerateUUID(), 505 }, 506 507 // Should be ignored as it is a different job. 508 &structs.Allocation{ 509 TaskGroup: tg1.Name, 510 JobID: "ignore 2", 511 ID: structs.GenerateUUID(), 512 }, 513 } 514 515 propsed := NewProposedAllocConstraintIterator(ctx, static) 516 propsed.SetTaskGroup(tg1) 517 propsed.SetJob(job) 518 519 out := collectFeasible(propsed) 520 if len(out) != 0 { 521 t.Fatalf("Bad: %#v", out) 522 } 523 } 524 525 func TestProposedAllocConstraint_JobDistinctHosts_InfeasibleCount(t *testing.T) { 526 _, ctx := testContext(t) 527 nodes := []*structs.Node{ 528 mock.Node(), 529 mock.Node(), 530 } 531 static := NewStaticIterator(ctx, nodes) 532 533 // Create a job with a distinct_hosts constraint and three task groups. 534 tg1 := &structs.TaskGroup{Name: "bar"} 535 tg2 := &structs.TaskGroup{Name: "baz"} 536 tg3 := &structs.TaskGroup{Name: "bam"} 537 538 job := &structs.Job{ 539 ID: "foo", 540 Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}}, 541 TaskGroups: []*structs.TaskGroup{tg1, tg2, tg3}, 542 } 543 544 propsed := NewProposedAllocConstraintIterator(ctx, static) 545 propsed.SetTaskGroup(tg1) 546 propsed.SetJob(job) 547 548 // It should not be able to place 3 tasks with only two nodes. 549 out := collectFeasible(propsed) 550 if len(out) != 2 { 551 t.Fatalf("Bad: %#v", out) 552 } 553 } 554 555 func TestProposedAllocConstraint_TaskGroupDistinctHosts(t *testing.T) { 556 _, ctx := testContext(t) 557 nodes := []*structs.Node{ 558 mock.Node(), 559 mock.Node(), 560 } 561 static := NewStaticIterator(ctx, nodes) 562 563 // Create a task group with a distinct_hosts constraint. 564 taskGroup := &structs.TaskGroup{ 565 Name: "example", 566 Constraints: []*structs.Constraint{ 567 {Operand: structs.ConstraintDistinctHosts}, 568 }, 569 } 570 571 // Add a planned alloc to node1. 572 plan := ctx.Plan() 573 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 574 &structs.Allocation{ 575 TaskGroup: taskGroup.Name, 576 JobID: "foo", 577 }, 578 } 579 580 // Add a planned alloc to node2 with the same task group name but a 581 // different job. 582 plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{ 583 &structs.Allocation{ 584 TaskGroup: taskGroup.Name, 585 JobID: "bar", 586 }, 587 } 588 589 propsed := NewProposedAllocConstraintIterator(ctx, static) 590 propsed.SetTaskGroup(taskGroup) 591 propsed.SetJob(&structs.Job{ID: "foo"}) 592 593 out := collectFeasible(propsed) 594 if len(out) != 1 { 595 t.Fatalf("Bad: %#v", out) 596 } 597 598 // Expect it to skip the first node as there is a previous alloc on it for 599 // the same task group. 600 if out[0] != nodes[1] { 601 t.Fatalf("Bad: %v", out) 602 } 603 } 604 605 func collectFeasible(iter FeasibleIterator) (out []*structs.Node) { 606 for { 607 next := iter.Next() 608 if next == nil { 609 break 610 } 611 out = append(out, next) 612 } 613 return 614 } 615 616 // mockFeasibilityChecker is a FeasibilityChecker that returns predetermined 617 // feasibility values. 618 type mockFeasibilityChecker struct { 619 retVals []bool 620 i int 621 } 622 623 func newMockFeasiblityChecker(values ...bool) *mockFeasibilityChecker { 624 return &mockFeasibilityChecker{retVals: values} 625 } 626 627 func (c *mockFeasibilityChecker) Feasible(*structs.Node) bool { 628 if c.i >= len(c.retVals) { 629 c.i++ 630 return false 631 } 632 633 f := c.retVals[c.i] 634 c.i++ 635 return f 636 } 637 638 // calls returns how many times the checker was called. 639 func (c *mockFeasibilityChecker) calls() int { return c.i } 640 641 func TestFeasibilityWrapper_JobIneligible(t *testing.T) { 642 _, ctx := testContext(t) 643 nodes := []*structs.Node{mock.Node()} 644 static := NewStaticIterator(ctx, nodes) 645 mocked := newMockFeasiblityChecker(false) 646 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil) 647 648 // Set the job to ineligible 649 ctx.Eligibility().SetJobEligibility(false, nodes[0].ComputedClass) 650 651 // Run the wrapper. 652 out := collectFeasible(wrapper) 653 654 if out != nil || mocked.calls() != 0 { 655 t.Fatalf("bad: %#v %d", out, mocked.calls()) 656 } 657 } 658 659 func TestFeasibilityWrapper_JobEscapes(t *testing.T) { 660 _, ctx := testContext(t) 661 nodes := []*structs.Node{mock.Node()} 662 static := NewStaticIterator(ctx, nodes) 663 mocked := newMockFeasiblityChecker(false) 664 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil) 665 666 // Set the job to escaped 667 cc := nodes[0].ComputedClass 668 ctx.Eligibility().job[cc] = EvalComputedClassEscaped 669 670 // Run the wrapper. 671 out := collectFeasible(wrapper) 672 673 if out != nil || mocked.calls() != 1 { 674 t.Fatalf("bad: %#v", out) 675 } 676 677 // Ensure that the job status didn't change from escaped even though the 678 // option failed. 679 if status := ctx.Eligibility().JobStatus(cc); status != EvalComputedClassEscaped { 680 t.Fatalf("job status is %v; want %v", status, EvalComputedClassEscaped) 681 } 682 } 683 684 func TestFeasibilityWrapper_JobAndTg_Eligible(t *testing.T) { 685 _, ctx := testContext(t) 686 nodes := []*structs.Node{mock.Node()} 687 static := NewStaticIterator(ctx, nodes) 688 jobMock := newMockFeasiblityChecker(true) 689 tgMock := newMockFeasiblityChecker(false) 690 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 691 692 // Set the job to escaped 693 cc := nodes[0].ComputedClass 694 ctx.Eligibility().job[cc] = EvalComputedClassEligible 695 ctx.Eligibility().SetTaskGroupEligibility(true, "foo", cc) 696 wrapper.SetTaskGroup("foo") 697 698 // Run the wrapper. 699 out := collectFeasible(wrapper) 700 701 if out == nil || tgMock.calls() != 0 { 702 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 703 } 704 } 705 706 func TestFeasibilityWrapper_JobEligible_TgIneligible(t *testing.T) { 707 _, ctx := testContext(t) 708 nodes := []*structs.Node{mock.Node()} 709 static := NewStaticIterator(ctx, nodes) 710 jobMock := newMockFeasiblityChecker(true) 711 tgMock := newMockFeasiblityChecker(false) 712 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 713 714 // Set the job to escaped 715 cc := nodes[0].ComputedClass 716 ctx.Eligibility().job[cc] = EvalComputedClassEligible 717 ctx.Eligibility().SetTaskGroupEligibility(false, "foo", cc) 718 wrapper.SetTaskGroup("foo") 719 720 // Run the wrapper. 721 out := collectFeasible(wrapper) 722 723 if out != nil || tgMock.calls() != 0 { 724 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 725 } 726 } 727 728 func TestFeasibilityWrapper_JobEligible_TgEscaped(t *testing.T) { 729 _, ctx := testContext(t) 730 nodes := []*structs.Node{mock.Node()} 731 static := NewStaticIterator(ctx, nodes) 732 jobMock := newMockFeasiblityChecker(true) 733 tgMock := newMockFeasiblityChecker(true) 734 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 735 736 // Set the job to escaped 737 cc := nodes[0].ComputedClass 738 ctx.Eligibility().job[cc] = EvalComputedClassEligible 739 ctx.Eligibility().taskGroups["foo"] = 740 map[string]ComputedClassFeasibility{cc: EvalComputedClassEscaped} 741 wrapper.SetTaskGroup("foo") 742 743 // Run the wrapper. 744 out := collectFeasible(wrapper) 745 746 if out == nil || tgMock.calls() != 1 { 747 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 748 } 749 750 if e, ok := ctx.Eligibility().taskGroups["foo"][cc]; !ok || e != EvalComputedClassEscaped { 751 t.Fatalf("bad: %v %v", e, ok) 752 } 753 }