github.com/ncodes/nomad@v0.5.7-0.20170403112158-97adf4a74fb3/scheduler/feasible_test.go (about) 1 package scheduler 2 3 import ( 4 "fmt" 5 "reflect" 6 "testing" 7 8 "github.com/ncodes/nomad/nomad/mock" 9 "github.com/ncodes/nomad/nomad/structs" 10 ) 11 12 func TestStaticIterator_Reset(t *testing.T) { 13 _, ctx := testContext(t) 14 var nodes []*structs.Node 15 for i := 0; i < 3; i++ { 16 nodes = append(nodes, mock.Node()) 17 } 18 static := NewStaticIterator(ctx, nodes) 19 20 for i := 0; i < 6; i++ { 21 static.Reset() 22 for j := 0; j < i; j++ { 23 static.Next() 24 } 25 static.Reset() 26 27 out := collectFeasible(static) 28 if len(out) != len(nodes) { 29 t.Fatalf("out: %#v", out) 30 t.Fatalf("missing nodes %d %#v", i, static) 31 } 32 33 ids := make(map[string]struct{}) 34 for _, o := range out { 35 if _, ok := ids[o.ID]; ok { 36 t.Fatalf("duplicate") 37 } 38 ids[o.ID] = struct{}{} 39 } 40 } 41 } 42 43 func TestStaticIterator_SetNodes(t *testing.T) { 44 _, ctx := testContext(t) 45 var nodes []*structs.Node 46 for i := 0; i < 3; i++ { 47 nodes = append(nodes, mock.Node()) 48 } 49 static := NewStaticIterator(ctx, nodes) 50 51 newNodes := []*structs.Node{mock.Node()} 52 static.SetNodes(newNodes) 53 54 out := collectFeasible(static) 55 if !reflect.DeepEqual(out, newNodes) { 56 t.Fatalf("bad: %#v", out) 57 } 58 } 59 60 func TestRandomIterator(t *testing.T) { 61 _, ctx := testContext(t) 62 var nodes []*structs.Node 63 for i := 0; i < 10; i++ { 64 nodes = append(nodes, mock.Node()) 65 } 66 67 nc := make([]*structs.Node, len(nodes)) 68 copy(nc, nodes) 69 rand := NewRandomIterator(ctx, nc) 70 71 out := collectFeasible(rand) 72 if len(out) != len(nodes) { 73 t.Fatalf("missing nodes") 74 } 75 if reflect.DeepEqual(out, nodes) { 76 t.Fatalf("same order") 77 } 78 } 79 80 func TestDriverChecker(t *testing.T) { 81 _, ctx := testContext(t) 82 nodes := []*structs.Node{ 83 mock.Node(), 84 mock.Node(), 85 mock.Node(), 86 mock.Node(), 87 } 88 nodes[0].Attributes["driver.foo"] = "1" 89 nodes[1].Attributes["driver.foo"] = "0" 90 nodes[2].Attributes["driver.foo"] = "true" 91 nodes[3].Attributes["driver.foo"] = "False" 92 93 drivers := map[string]struct{}{ 94 "exec": struct{}{}, 95 "foo": struct{}{}, 96 } 97 checker := NewDriverChecker(ctx, drivers) 98 cases := []struct { 99 Node *structs.Node 100 Result bool 101 }{ 102 { 103 Node: nodes[0], 104 Result: true, 105 }, 106 { 107 Node: nodes[1], 108 Result: false, 109 }, 110 { 111 Node: nodes[2], 112 Result: true, 113 }, 114 { 115 Node: nodes[3], 116 Result: false, 117 }, 118 } 119 120 for i, c := range cases { 121 if act := checker.Feasible(c.Node); act != c.Result { 122 t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result) 123 } 124 } 125 } 126 127 func TestConstraintChecker(t *testing.T) { 128 _, ctx := testContext(t) 129 nodes := []*structs.Node{ 130 mock.Node(), 131 mock.Node(), 132 mock.Node(), 133 mock.Node(), 134 } 135 136 nodes[0].Attributes["kernel.name"] = "freebsd" 137 nodes[1].Datacenter = "dc2" 138 nodes[2].NodeClass = "large" 139 140 constraints := []*structs.Constraint{ 141 &structs.Constraint{ 142 Operand: "=", 143 LTarget: "${node.datacenter}", 144 RTarget: "dc1", 145 }, 146 &structs.Constraint{ 147 Operand: "is", 148 LTarget: "${attr.kernel.name}", 149 RTarget: "linux", 150 }, 151 &structs.Constraint{ 152 Operand: "is", 153 LTarget: "${node.class}", 154 RTarget: "large", 155 }, 156 } 157 checker := NewConstraintChecker(ctx, constraints) 158 cases := []struct { 159 Node *structs.Node 160 Result bool 161 }{ 162 { 163 Node: nodes[0], 164 Result: false, 165 }, 166 { 167 Node: nodes[1], 168 Result: false, 169 }, 170 { 171 Node: nodes[2], 172 Result: true, 173 }, 174 } 175 176 for i, c := range cases { 177 if act := checker.Feasible(c.Node); act != c.Result { 178 t.Fatalf("case(%d) failed: got %v; want %v", i, act, c.Result) 179 } 180 } 181 } 182 183 func TestResolveConstraintTarget(t *testing.T) { 184 type tcase struct { 185 target string 186 node *structs.Node 187 val interface{} 188 result bool 189 } 190 node := mock.Node() 191 cases := []tcase{ 192 { 193 target: "${node.unique.id}", 194 node: node, 195 val: node.ID, 196 result: true, 197 }, 198 { 199 target: "${node.datacenter}", 200 node: node, 201 val: node.Datacenter, 202 result: true, 203 }, 204 { 205 target: "${node.unique.name}", 206 node: node, 207 val: node.Name, 208 result: true, 209 }, 210 { 211 target: "${node.class}", 212 node: node, 213 val: node.NodeClass, 214 result: true, 215 }, 216 { 217 target: "${node.foo}", 218 node: node, 219 result: false, 220 }, 221 { 222 target: "${attr.kernel.name}", 223 node: node, 224 val: node.Attributes["kernel.name"], 225 result: true, 226 }, 227 { 228 target: "${attr.rand}", 229 node: node, 230 result: false, 231 }, 232 { 233 target: "${meta.pci-dss}", 234 node: node, 235 val: node.Meta["pci-dss"], 236 result: true, 237 }, 238 { 239 target: "${meta.rand}", 240 node: node, 241 result: false, 242 }, 243 } 244 245 for _, tc := range cases { 246 res, ok := resolveConstraintTarget(tc.target, tc.node) 247 if ok != tc.result { 248 t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok) 249 } 250 if ok && !reflect.DeepEqual(res, tc.val) { 251 t.Fatalf("TC: %#v, Result: %v %v", tc, res, ok) 252 } 253 } 254 } 255 256 func TestCheckConstraint(t *testing.T) { 257 type tcase struct { 258 op string 259 lVal, rVal interface{} 260 result bool 261 } 262 cases := []tcase{ 263 { 264 op: "=", 265 lVal: "foo", rVal: "foo", 266 result: true, 267 }, 268 { 269 op: "is", 270 lVal: "foo", rVal: "foo", 271 result: true, 272 }, 273 { 274 op: "==", 275 lVal: "foo", rVal: "foo", 276 result: true, 277 }, 278 { 279 op: "!=", 280 lVal: "foo", rVal: "foo", 281 result: false, 282 }, 283 { 284 op: "!=", 285 lVal: "foo", rVal: "bar", 286 result: true, 287 }, 288 { 289 op: "not", 290 lVal: "foo", rVal: "bar", 291 result: true, 292 }, 293 { 294 op: structs.ConstraintVersion, 295 lVal: "1.2.3", rVal: "~> 1.0", 296 result: true, 297 }, 298 { 299 op: structs.ConstraintRegex, 300 lVal: "foobarbaz", rVal: "[\\w]+", 301 result: true, 302 }, 303 { 304 op: "<", 305 lVal: "foo", rVal: "bar", 306 result: false, 307 }, 308 { 309 op: structs.ConstraintSetContains, 310 lVal: "foo,bar,baz", rVal: "foo, bar ", 311 result: true, 312 }, 313 { 314 op: structs.ConstraintSetContains, 315 lVal: "foo,bar,baz", rVal: "foo,bam", 316 result: false, 317 }, 318 } 319 320 for _, tc := range cases { 321 _, ctx := testContext(t) 322 if res := checkConstraint(ctx, tc.op, tc.lVal, tc.rVal); res != tc.result { 323 t.Fatalf("TC: %#v, Result: %v", tc, res) 324 } 325 } 326 } 327 328 func TestCheckLexicalOrder(t *testing.T) { 329 type tcase struct { 330 op string 331 lVal, rVal interface{} 332 result bool 333 } 334 cases := []tcase{ 335 { 336 op: "<", 337 lVal: "bar", rVal: "foo", 338 result: true, 339 }, 340 { 341 op: "<=", 342 lVal: "foo", rVal: "foo", 343 result: true, 344 }, 345 { 346 op: ">", 347 lVal: "bar", rVal: "foo", 348 result: false, 349 }, 350 { 351 op: ">=", 352 lVal: "bar", rVal: "bar", 353 result: true, 354 }, 355 { 356 op: ">", 357 lVal: 1, rVal: "foo", 358 result: false, 359 }, 360 } 361 for _, tc := range cases { 362 if res := checkLexicalOrder(tc.op, tc.lVal, tc.rVal); res != tc.result { 363 t.Fatalf("TC: %#v, Result: %v", tc, res) 364 } 365 } 366 } 367 368 func TestCheckVersionConstraint(t *testing.T) { 369 type tcase struct { 370 lVal, rVal interface{} 371 result bool 372 } 373 cases := []tcase{ 374 { 375 lVal: "1.2.3", rVal: "~> 1.0", 376 result: true, 377 }, 378 { 379 lVal: "1.2.3", rVal: ">= 1.0, < 1.4", 380 result: true, 381 }, 382 { 383 lVal: "2.0.1", rVal: "~> 1.0", 384 result: false, 385 }, 386 { 387 lVal: "1.4", rVal: ">= 1.0, < 1.4", 388 result: false, 389 }, 390 { 391 lVal: 1, rVal: "~> 1.0", 392 result: true, 393 }, 394 } 395 for _, tc := range cases { 396 _, ctx := testContext(t) 397 if res := checkVersionConstraint(ctx, tc.lVal, tc.rVal); res != tc.result { 398 t.Fatalf("TC: %#v, Result: %v", tc, res) 399 } 400 } 401 } 402 403 func TestCheckRegexpConstraint(t *testing.T) { 404 type tcase struct { 405 lVal, rVal interface{} 406 result bool 407 } 408 cases := []tcase{ 409 { 410 lVal: "foobar", rVal: "bar", 411 result: true, 412 }, 413 { 414 lVal: "foobar", rVal: "^foo", 415 result: true, 416 }, 417 { 418 lVal: "foobar", rVal: "^bar", 419 result: false, 420 }, 421 { 422 lVal: "zipzap", rVal: "foo", 423 result: false, 424 }, 425 { 426 lVal: 1, rVal: "foo", 427 result: false, 428 }, 429 } 430 for _, tc := range cases { 431 _, ctx := testContext(t) 432 if res := checkRegexpConstraint(ctx, tc.lVal, tc.rVal); res != tc.result { 433 t.Fatalf("TC: %#v, Result: %v", tc, res) 434 } 435 } 436 } 437 438 // This test puts allocations on the node to test if it detects infeasibility of 439 // nodes correctly and picks the only feasible one 440 func TestDistinctHostsIterator_JobDistinctHosts(t *testing.T) { 441 _, ctx := testContext(t) 442 nodes := []*structs.Node{ 443 mock.Node(), 444 mock.Node(), 445 mock.Node(), 446 } 447 static := NewStaticIterator(ctx, nodes) 448 449 // Create a job with a distinct_hosts constraint and two task groups. 450 tg1 := &structs.TaskGroup{Name: "bar"} 451 tg2 := &structs.TaskGroup{Name: "baz"} 452 453 job := &structs.Job{ 454 ID: "foo", 455 Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}}, 456 TaskGroups: []*structs.TaskGroup{tg1, tg2}, 457 } 458 459 // Add allocs placing tg1 on node1 and tg2 on node2. This should make the 460 // job unsatisfiable on all nodes but node3 461 plan := ctx.Plan() 462 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 463 &structs.Allocation{ 464 TaskGroup: tg1.Name, 465 JobID: job.ID, 466 ID: structs.GenerateUUID(), 467 }, 468 469 // Should be ignored as it is a different job. 470 &structs.Allocation{ 471 TaskGroup: tg2.Name, 472 JobID: "ignore 2", 473 ID: structs.GenerateUUID(), 474 }, 475 } 476 plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{ 477 &structs.Allocation{ 478 TaskGroup: tg2.Name, 479 JobID: job.ID, 480 ID: structs.GenerateUUID(), 481 }, 482 483 // Should be ignored as it is a different job. 484 &structs.Allocation{ 485 TaskGroup: tg1.Name, 486 JobID: "ignore 2", 487 ID: structs.GenerateUUID(), 488 }, 489 } 490 491 proposed := NewDistinctHostsIterator(ctx, static) 492 proposed.SetTaskGroup(tg1) 493 proposed.SetJob(job) 494 495 out := collectFeasible(proposed) 496 if len(out) != 1 { 497 t.Fatalf("Bad: %#v", out) 498 } 499 500 if out[0].ID != nodes[2].ID { 501 t.Fatalf("wrong node picked") 502 } 503 } 504 505 func TestDistinctHostsIterator_JobDistinctHosts_InfeasibleCount(t *testing.T) { 506 _, ctx := testContext(t) 507 nodes := []*structs.Node{ 508 mock.Node(), 509 mock.Node(), 510 } 511 static := NewStaticIterator(ctx, nodes) 512 513 // Create a job with a distinct_hosts constraint and three task groups. 514 tg1 := &structs.TaskGroup{Name: "bar"} 515 tg2 := &structs.TaskGroup{Name: "baz"} 516 tg3 := &structs.TaskGroup{Name: "bam"} 517 518 job := &structs.Job{ 519 ID: "foo", 520 Constraints: []*structs.Constraint{{Operand: structs.ConstraintDistinctHosts}}, 521 TaskGroups: []*structs.TaskGroup{tg1, tg2, tg3}, 522 } 523 524 // Add allocs placing tg1 on node1 and tg2 on node2. This should make the 525 // job unsatisfiable for tg3 526 plan := ctx.Plan() 527 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 528 &structs.Allocation{ 529 TaskGroup: tg1.Name, 530 JobID: job.ID, 531 ID: structs.GenerateUUID(), 532 }, 533 } 534 plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{ 535 &structs.Allocation{ 536 TaskGroup: tg2.Name, 537 JobID: job.ID, 538 ID: structs.GenerateUUID(), 539 }, 540 } 541 542 proposed := NewDistinctHostsIterator(ctx, static) 543 proposed.SetTaskGroup(tg3) 544 proposed.SetJob(job) 545 546 // It should not be able to place 3 tasks with only two nodes. 547 out := collectFeasible(proposed) 548 if len(out) != 0 { 549 t.Fatalf("Bad: %#v", out) 550 } 551 } 552 553 func TestDistinctHostsIterator_TaskGroupDistinctHosts(t *testing.T) { 554 _, ctx := testContext(t) 555 nodes := []*structs.Node{ 556 mock.Node(), 557 mock.Node(), 558 } 559 static := NewStaticIterator(ctx, nodes) 560 561 // Create a task group with a distinct_hosts constraint. 562 tg1 := &structs.TaskGroup{ 563 Name: "example", 564 Constraints: []*structs.Constraint{ 565 {Operand: structs.ConstraintDistinctHosts}, 566 }, 567 } 568 tg2 := &structs.TaskGroup{Name: "baz"} 569 570 // Add a planned alloc to node1. 571 plan := ctx.Plan() 572 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 573 &structs.Allocation{ 574 TaskGroup: tg1.Name, 575 JobID: "foo", 576 }, 577 } 578 579 // Add a planned alloc to node2 with the same task group name but a 580 // different job. 581 plan.NodeAllocation[nodes[1].ID] = []*structs.Allocation{ 582 &structs.Allocation{ 583 TaskGroup: tg1.Name, 584 JobID: "bar", 585 }, 586 } 587 588 proposed := NewDistinctHostsIterator(ctx, static) 589 proposed.SetTaskGroup(tg1) 590 proposed.SetJob(&structs.Job{ID: "foo"}) 591 592 out := collectFeasible(proposed) 593 if len(out) != 1 { 594 t.Fatalf("Bad: %#v", out) 595 } 596 597 // Expect it to skip the first node as there is a previous alloc on it for 598 // the same task group. 599 if out[0] != nodes[1] { 600 t.Fatalf("Bad: %v", out) 601 } 602 603 // Since the other task group doesn't have the constraint, both nodes should 604 // be feasible. 605 proposed.Reset() 606 proposed.SetTaskGroup(tg2) 607 out = collectFeasible(proposed) 608 if len(out) != 2 { 609 t.Fatalf("Bad: %#v", out) 610 } 611 } 612 613 // This test puts creates allocations across task groups that use a property 614 // value to detect if the constraint at the job level properly considers all 615 // task groups. 616 func TestDistinctPropertyIterator_JobDistinctProperty(t *testing.T) { 617 state, ctx := testContext(t) 618 nodes := []*structs.Node{ 619 mock.Node(), 620 mock.Node(), 621 mock.Node(), 622 mock.Node(), 623 mock.Node(), 624 } 625 626 for i, n := range nodes { 627 n.Meta["rack"] = fmt.Sprintf("%d", i) 628 629 // Add to state store 630 if err := state.UpsertNode(uint64(100+i), n); err != nil { 631 t.Fatalf("failed to upsert node: %v", err) 632 } 633 } 634 635 static := NewStaticIterator(ctx, nodes) 636 637 // Create a job with a distinct_property constraint and a task groups. 638 tg1 := &structs.TaskGroup{Name: "bar"} 639 tg2 := &structs.TaskGroup{Name: "baz"} 640 641 job := &structs.Job{ 642 ID: "foo", 643 Constraints: []*structs.Constraint{ 644 { 645 Operand: structs.ConstraintDistinctProperty, 646 LTarget: "${meta.rack}", 647 }, 648 }, 649 TaskGroups: []*structs.TaskGroup{tg1, tg2}, 650 } 651 652 // Add allocs placing tg1 on node1 and 2 and tg2 on node3 and 4. This should make the 653 // job unsatisfiable on all nodes but node5. Also mix the allocations 654 // existing in the plan and the state store. 655 plan := ctx.Plan() 656 alloc1ID := structs.GenerateUUID() 657 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 658 &structs.Allocation{ 659 TaskGroup: tg1.Name, 660 JobID: job.ID, 661 ID: alloc1ID, 662 NodeID: nodes[0].ID, 663 }, 664 665 // Should be ignored as it is a different job. 666 &structs.Allocation{ 667 TaskGroup: tg2.Name, 668 JobID: "ignore 2", 669 ID: structs.GenerateUUID(), 670 NodeID: nodes[0].ID, 671 }, 672 } 673 plan.NodeAllocation[nodes[2].ID] = []*structs.Allocation{ 674 &structs.Allocation{ 675 TaskGroup: tg2.Name, 676 JobID: job.ID, 677 ID: structs.GenerateUUID(), 678 NodeID: nodes[2].ID, 679 }, 680 681 // Should be ignored as it is a different job. 682 &structs.Allocation{ 683 TaskGroup: tg1.Name, 684 JobID: "ignore 2", 685 ID: structs.GenerateUUID(), 686 NodeID: nodes[2].ID, 687 }, 688 } 689 690 // Put an allocation on Node 5 but make it stopped in the plan 691 stoppingAllocID := structs.GenerateUUID() 692 plan.NodeUpdate[nodes[4].ID] = []*structs.Allocation{ 693 &structs.Allocation{ 694 TaskGroup: tg2.Name, 695 JobID: job.ID, 696 ID: stoppingAllocID, 697 NodeID: nodes[4].ID, 698 }, 699 } 700 701 upserting := []*structs.Allocation{ 702 // Have one of the allocations exist in both the plan and the state 703 // store. This resembles an allocation update 704 &structs.Allocation{ 705 TaskGroup: tg1.Name, 706 JobID: job.ID, 707 ID: alloc1ID, 708 EvalID: structs.GenerateUUID(), 709 NodeID: nodes[0].ID, 710 }, 711 712 &structs.Allocation{ 713 TaskGroup: tg1.Name, 714 JobID: job.ID, 715 ID: structs.GenerateUUID(), 716 EvalID: structs.GenerateUUID(), 717 NodeID: nodes[1].ID, 718 }, 719 720 // Should be ignored as it is a different job. 721 &structs.Allocation{ 722 TaskGroup: tg2.Name, 723 JobID: "ignore 2", 724 ID: structs.GenerateUUID(), 725 EvalID: structs.GenerateUUID(), 726 NodeID: nodes[1].ID, 727 }, 728 &structs.Allocation{ 729 TaskGroup: tg2.Name, 730 JobID: job.ID, 731 ID: structs.GenerateUUID(), 732 EvalID: structs.GenerateUUID(), 733 NodeID: nodes[3].ID, 734 }, 735 736 // Should be ignored as it is a different job. 737 &structs.Allocation{ 738 TaskGroup: tg1.Name, 739 JobID: "ignore 2", 740 ID: structs.GenerateUUID(), 741 EvalID: structs.GenerateUUID(), 742 NodeID: nodes[3].ID, 743 }, 744 &structs.Allocation{ 745 TaskGroup: tg2.Name, 746 JobID: job.ID, 747 ID: stoppingAllocID, 748 EvalID: structs.GenerateUUID(), 749 NodeID: nodes[4].ID, 750 }, 751 } 752 if err := state.UpsertAllocs(1000, upserting); err != nil { 753 t.Fatalf("failed to UpsertAllocs: %v", err) 754 } 755 756 proposed := NewDistinctPropertyIterator(ctx, static) 757 proposed.SetJob(job) 758 proposed.SetTaskGroup(tg2) 759 proposed.Reset() 760 761 out := collectFeasible(proposed) 762 if len(out) != 1 { 763 t.Fatalf("Bad: %#v", out) 764 } 765 if out[0].ID != nodes[4].ID { 766 t.Fatalf("wrong node picked") 767 } 768 } 769 770 // This test checks that if a node has an allocation on it that gets stopped, 771 // there is a plan to re-use that for a new allocation, that the next select 772 // won't select that node. 773 func TestDistinctPropertyIterator_JobDistinctProperty_RemoveAndReplace(t *testing.T) { 774 state, ctx := testContext(t) 775 nodes := []*structs.Node{ 776 mock.Node(), 777 } 778 779 nodes[0].Meta["rack"] = "1" 780 781 // Add to state store 782 if err := state.UpsertNode(uint64(100), nodes[0]); err != nil { 783 t.Fatalf("failed to upsert node: %v", err) 784 } 785 786 static := NewStaticIterator(ctx, nodes) 787 788 // Create a job with a distinct_property constraint and a task groups. 789 tg1 := &structs.TaskGroup{Name: "bar"} 790 job := &structs.Job{ 791 ID: "foo", 792 Constraints: []*structs.Constraint{ 793 { 794 Operand: structs.ConstraintDistinctProperty, 795 LTarget: "${meta.rack}", 796 }, 797 }, 798 TaskGroups: []*structs.TaskGroup{tg1}, 799 } 800 801 plan := ctx.Plan() 802 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 803 &structs.Allocation{ 804 TaskGroup: tg1.Name, 805 JobID: job.ID, 806 ID: structs.GenerateUUID(), 807 NodeID: nodes[0].ID, 808 }, 809 } 810 811 stoppingAllocID := structs.GenerateUUID() 812 plan.NodeUpdate[nodes[0].ID] = []*structs.Allocation{ 813 &structs.Allocation{ 814 TaskGroup: tg1.Name, 815 JobID: job.ID, 816 ID: stoppingAllocID, 817 NodeID: nodes[0].ID, 818 }, 819 } 820 821 upserting := []*structs.Allocation{ 822 &structs.Allocation{ 823 TaskGroup: tg1.Name, 824 JobID: job.ID, 825 ID: stoppingAllocID, 826 EvalID: structs.GenerateUUID(), 827 NodeID: nodes[0].ID, 828 }, 829 } 830 if err := state.UpsertAllocs(1000, upserting); err != nil { 831 t.Fatalf("failed to UpsertAllocs: %v", err) 832 } 833 834 proposed := NewDistinctPropertyIterator(ctx, static) 835 proposed.SetJob(job) 836 proposed.SetTaskGroup(tg1) 837 proposed.Reset() 838 839 out := collectFeasible(proposed) 840 if len(out) != 0 { 841 t.Fatalf("Bad: %#v", out) 842 } 843 } 844 845 // This test creates previous allocations selecting certain property values to 846 // test if it detects infeasibility of property values correctly and picks the 847 // only feasible one 848 func TestDistinctPropertyIterator_JobDistinctProperty_Infeasible(t *testing.T) { 849 state, ctx := testContext(t) 850 nodes := []*structs.Node{ 851 mock.Node(), 852 mock.Node(), 853 } 854 855 for i, n := range nodes { 856 n.Meta["rack"] = fmt.Sprintf("%d", i) 857 858 // Add to state store 859 if err := state.UpsertNode(uint64(100+i), n); err != nil { 860 t.Fatalf("failed to upsert node: %v", err) 861 } 862 } 863 864 static := NewStaticIterator(ctx, nodes) 865 866 // Create a job with a distinct_property constraint and a task groups. 867 tg1 := &structs.TaskGroup{Name: "bar"} 868 tg2 := &structs.TaskGroup{Name: "baz"} 869 tg3 := &structs.TaskGroup{Name: "bam"} 870 871 job := &structs.Job{ 872 ID: "foo", 873 Constraints: []*structs.Constraint{ 874 { 875 Operand: structs.ConstraintDistinctProperty, 876 LTarget: "${meta.rack}", 877 }, 878 }, 879 TaskGroups: []*structs.TaskGroup{tg1, tg2, tg3}, 880 } 881 882 // Add allocs placing tg1 on node1 and tg2 on node2. This should make the 883 // job unsatisfiable for tg3. 884 plan := ctx.Plan() 885 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 886 &structs.Allocation{ 887 TaskGroup: tg1.Name, 888 JobID: job.ID, 889 ID: structs.GenerateUUID(), 890 NodeID: nodes[0].ID, 891 }, 892 } 893 upserting := []*structs.Allocation{ 894 &structs.Allocation{ 895 TaskGroup: tg2.Name, 896 JobID: job.ID, 897 ID: structs.GenerateUUID(), 898 EvalID: structs.GenerateUUID(), 899 NodeID: nodes[1].ID, 900 }, 901 } 902 if err := state.UpsertAllocs(1000, upserting); err != nil { 903 t.Fatalf("failed to UpsertAllocs: %v", err) 904 } 905 906 proposed := NewDistinctPropertyIterator(ctx, static) 907 proposed.SetJob(job) 908 proposed.SetTaskGroup(tg3) 909 proposed.Reset() 910 911 out := collectFeasible(proposed) 912 if len(out) != 0 { 913 t.Fatalf("Bad: %#v", out) 914 } 915 } 916 917 // This test creates previous allocations selecting certain property values to 918 // test if it detects infeasibility of property values correctly and picks the 919 // only feasible one when the constraint is at the task group. 920 func TestDistinctPropertyIterator_TaskGroupDistinctProperty(t *testing.T) { 921 state, ctx := testContext(t) 922 nodes := []*structs.Node{ 923 mock.Node(), 924 mock.Node(), 925 mock.Node(), 926 } 927 928 for i, n := range nodes { 929 n.Meta["rack"] = fmt.Sprintf("%d", i) 930 931 // Add to state store 932 if err := state.UpsertNode(uint64(100+i), n); err != nil { 933 t.Fatalf("failed to upsert node: %v", err) 934 } 935 } 936 937 static := NewStaticIterator(ctx, nodes) 938 939 // Create a job with a task group with the distinct_property constraint 940 tg1 := &structs.TaskGroup{ 941 Name: "example", 942 Constraints: []*structs.Constraint{ 943 { 944 Operand: structs.ConstraintDistinctProperty, 945 LTarget: "${meta.rack}", 946 }, 947 }, 948 } 949 tg2 := &structs.TaskGroup{Name: "baz"} 950 951 job := &structs.Job{ 952 ID: "foo", 953 TaskGroups: []*structs.TaskGroup{tg1, tg2}, 954 } 955 956 // Add allocs placing tg1 on node1 and 2. This should make the 957 // job unsatisfiable on all nodes but node3. Also mix the allocations 958 // existing in the plan and the state store. 959 plan := ctx.Plan() 960 plan.NodeAllocation[nodes[0].ID] = []*structs.Allocation{ 961 &structs.Allocation{ 962 TaskGroup: tg1.Name, 963 JobID: job.ID, 964 ID: structs.GenerateUUID(), 965 NodeID: nodes[0].ID, 966 }, 967 } 968 969 // Put an allocation on Node 3 but make it stopped in the plan 970 stoppingAllocID := structs.GenerateUUID() 971 plan.NodeUpdate[nodes[2].ID] = []*structs.Allocation{ 972 &structs.Allocation{ 973 TaskGroup: tg1.Name, 974 JobID: job.ID, 975 ID: stoppingAllocID, 976 NodeID: nodes[2].ID, 977 }, 978 } 979 980 upserting := []*structs.Allocation{ 981 &structs.Allocation{ 982 TaskGroup: tg1.Name, 983 JobID: job.ID, 984 ID: structs.GenerateUUID(), 985 EvalID: structs.GenerateUUID(), 986 NodeID: nodes[1].ID, 987 }, 988 989 // Should be ignored as it is a different job. 990 &structs.Allocation{ 991 TaskGroup: tg1.Name, 992 JobID: "ignore 2", 993 ID: structs.GenerateUUID(), 994 EvalID: structs.GenerateUUID(), 995 NodeID: nodes[2].ID, 996 }, 997 998 &structs.Allocation{ 999 TaskGroup: tg1.Name, 1000 JobID: job.ID, 1001 ID: stoppingAllocID, 1002 EvalID: structs.GenerateUUID(), 1003 NodeID: nodes[2].ID, 1004 }, 1005 } 1006 if err := state.UpsertAllocs(1000, upserting); err != nil { 1007 t.Fatalf("failed to UpsertAllocs: %v", err) 1008 } 1009 1010 proposed := NewDistinctPropertyIterator(ctx, static) 1011 proposed.SetJob(job) 1012 proposed.SetTaskGroup(tg1) 1013 proposed.Reset() 1014 1015 out := collectFeasible(proposed) 1016 if len(out) != 1 { 1017 t.Fatalf("Bad: %#v", out) 1018 } 1019 if out[0].ID != nodes[2].ID { 1020 t.Fatalf("wrong node picked") 1021 } 1022 1023 // Since the other task group doesn't have the constraint, both nodes should 1024 // be feasible. 1025 proposed.SetTaskGroup(tg2) 1026 proposed.Reset() 1027 1028 out = collectFeasible(proposed) 1029 if len(out) != 3 { 1030 t.Fatalf("Bad: %#v", out) 1031 } 1032 } 1033 1034 func collectFeasible(iter FeasibleIterator) (out []*structs.Node) { 1035 for { 1036 next := iter.Next() 1037 if next == nil { 1038 break 1039 } 1040 out = append(out, next) 1041 } 1042 return 1043 } 1044 1045 // mockFeasibilityChecker is a FeasibilityChecker that returns predetermined 1046 // feasibility values. 1047 type mockFeasibilityChecker struct { 1048 retVals []bool 1049 i int 1050 } 1051 1052 func newMockFeasiblityChecker(values ...bool) *mockFeasibilityChecker { 1053 return &mockFeasibilityChecker{retVals: values} 1054 } 1055 1056 func (c *mockFeasibilityChecker) Feasible(*structs.Node) bool { 1057 if c.i >= len(c.retVals) { 1058 c.i++ 1059 return false 1060 } 1061 1062 f := c.retVals[c.i] 1063 c.i++ 1064 return f 1065 } 1066 1067 // calls returns how many times the checker was called. 1068 func (c *mockFeasibilityChecker) calls() int { return c.i } 1069 1070 func TestFeasibilityWrapper_JobIneligible(t *testing.T) { 1071 _, ctx := testContext(t) 1072 nodes := []*structs.Node{mock.Node()} 1073 static := NewStaticIterator(ctx, nodes) 1074 mocked := newMockFeasiblityChecker(false) 1075 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil) 1076 1077 // Set the job to ineligible 1078 ctx.Eligibility().SetJobEligibility(false, nodes[0].ComputedClass) 1079 1080 // Run the wrapper. 1081 out := collectFeasible(wrapper) 1082 1083 if out != nil || mocked.calls() != 0 { 1084 t.Fatalf("bad: %#v %d", out, mocked.calls()) 1085 } 1086 } 1087 1088 func TestFeasibilityWrapper_JobEscapes(t *testing.T) { 1089 _, ctx := testContext(t) 1090 nodes := []*structs.Node{mock.Node()} 1091 static := NewStaticIterator(ctx, nodes) 1092 mocked := newMockFeasiblityChecker(false) 1093 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{mocked}, nil) 1094 1095 // Set the job to escaped 1096 cc := nodes[0].ComputedClass 1097 ctx.Eligibility().job[cc] = EvalComputedClassEscaped 1098 1099 // Run the wrapper. 1100 out := collectFeasible(wrapper) 1101 1102 if out != nil || mocked.calls() != 1 { 1103 t.Fatalf("bad: %#v", out) 1104 } 1105 1106 // Ensure that the job status didn't change from escaped even though the 1107 // option failed. 1108 if status := ctx.Eligibility().JobStatus(cc); status != EvalComputedClassEscaped { 1109 t.Fatalf("job status is %v; want %v", status, EvalComputedClassEscaped) 1110 } 1111 } 1112 1113 func TestFeasibilityWrapper_JobAndTg_Eligible(t *testing.T) { 1114 _, ctx := testContext(t) 1115 nodes := []*structs.Node{mock.Node()} 1116 static := NewStaticIterator(ctx, nodes) 1117 jobMock := newMockFeasiblityChecker(true) 1118 tgMock := newMockFeasiblityChecker(false) 1119 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 1120 1121 // Set the job to escaped 1122 cc := nodes[0].ComputedClass 1123 ctx.Eligibility().job[cc] = EvalComputedClassEligible 1124 ctx.Eligibility().SetTaskGroupEligibility(true, "foo", cc) 1125 wrapper.SetTaskGroup("foo") 1126 1127 // Run the wrapper. 1128 out := collectFeasible(wrapper) 1129 1130 if out == nil || tgMock.calls() != 0 { 1131 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 1132 } 1133 } 1134 1135 func TestFeasibilityWrapper_JobEligible_TgIneligible(t *testing.T) { 1136 _, ctx := testContext(t) 1137 nodes := []*structs.Node{mock.Node()} 1138 static := NewStaticIterator(ctx, nodes) 1139 jobMock := newMockFeasiblityChecker(true) 1140 tgMock := newMockFeasiblityChecker(false) 1141 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 1142 1143 // Set the job to escaped 1144 cc := nodes[0].ComputedClass 1145 ctx.Eligibility().job[cc] = EvalComputedClassEligible 1146 ctx.Eligibility().SetTaskGroupEligibility(false, "foo", cc) 1147 wrapper.SetTaskGroup("foo") 1148 1149 // Run the wrapper. 1150 out := collectFeasible(wrapper) 1151 1152 if out != nil || tgMock.calls() != 0 { 1153 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 1154 } 1155 } 1156 1157 func TestFeasibilityWrapper_JobEligible_TgEscaped(t *testing.T) { 1158 _, ctx := testContext(t) 1159 nodes := []*structs.Node{mock.Node()} 1160 static := NewStaticIterator(ctx, nodes) 1161 jobMock := newMockFeasiblityChecker(true) 1162 tgMock := newMockFeasiblityChecker(true) 1163 wrapper := NewFeasibilityWrapper(ctx, static, []FeasibilityChecker{jobMock}, []FeasibilityChecker{tgMock}) 1164 1165 // Set the job to escaped 1166 cc := nodes[0].ComputedClass 1167 ctx.Eligibility().job[cc] = EvalComputedClassEligible 1168 ctx.Eligibility().taskGroups["foo"] = 1169 map[string]ComputedClassFeasibility{cc: EvalComputedClassEscaped} 1170 wrapper.SetTaskGroup("foo") 1171 1172 // Run the wrapper. 1173 out := collectFeasible(wrapper) 1174 1175 if out == nil || tgMock.calls() != 1 { 1176 t.Fatalf("bad: %#v %v", out, tgMock.calls()) 1177 } 1178 1179 if e, ok := ctx.Eligibility().taskGroups["foo"][cc]; !ok || e != EvalComputedClassEscaped { 1180 t.Fatalf("bad: %v %v", e, ok) 1181 } 1182 }