github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/scheduler/rank_test.go (about) 1 package scheduler 2 3 import ( 4 "testing" 5 6 "github.com/hashicorp/nomad/helper/uuid" 7 "github.com/hashicorp/nomad/nomad/mock" 8 "github.com/hashicorp/nomad/nomad/structs" 9 require "github.com/stretchr/testify/require" 10 ) 11 12 func TestFeasibleRankIterator(t *testing.T) { 13 _, ctx := testContext(t) 14 var nodes []*structs.Node 15 for i := 0; i < 10; i++ { 16 nodes = append(nodes, mock.Node()) 17 } 18 static := NewStaticIterator(ctx, nodes) 19 20 feasible := NewFeasibleRankIterator(ctx, static) 21 22 out := collectRanked(feasible) 23 if len(out) != len(nodes) { 24 t.Fatalf("bad: %v", out) 25 } 26 } 27 28 func TestBinPackIterator_NoExistingAlloc(t *testing.T) { 29 _, ctx := testContext(t) 30 nodes := []*RankedNode{ 31 { 32 Node: &structs.Node{ 33 // Perfect fit 34 Resources: &structs.Resources{ 35 CPU: 2048, 36 MemoryMB: 2048, 37 }, 38 Reserved: &structs.Resources{ 39 CPU: 1024, 40 MemoryMB: 1024, 41 }, 42 }, 43 }, 44 { 45 Node: &structs.Node{ 46 // Overloaded 47 Resources: &structs.Resources{ 48 CPU: 1024, 49 MemoryMB: 1024, 50 }, 51 Reserved: &structs.Resources{ 52 CPU: 512, 53 MemoryMB: 512, 54 }, 55 }, 56 }, 57 { 58 Node: &structs.Node{ 59 // 50% fit 60 Resources: &structs.Resources{ 61 CPU: 4096, 62 MemoryMB: 4096, 63 }, 64 Reserved: &structs.Resources{ 65 CPU: 1024, 66 MemoryMB: 1024, 67 }, 68 }, 69 }, 70 } 71 static := NewStaticRankIterator(ctx, nodes) 72 73 taskGroup := &structs.TaskGroup{ 74 EphemeralDisk: &structs.EphemeralDisk{}, 75 Tasks: []*structs.Task{ 76 { 77 Name: "web", 78 Resources: &structs.Resources{ 79 CPU: 1024, 80 MemoryMB: 1024, 81 }, 82 }, 83 }, 84 } 85 binp := NewBinPackIterator(ctx, static, false, 0) 86 binp.SetTaskGroup(taskGroup) 87 88 scoreNorm := NewScoreNormalizationIterator(ctx, binp) 89 90 out := collectRanked(scoreNorm) 91 if len(out) != 2 { 92 t.Fatalf("Bad: %v", out) 93 } 94 if out[0] != nodes[0] || out[1] != nodes[2] { 95 t.Fatalf("Bad: %v", out) 96 } 97 98 if out[0].FinalScore != 1.0 { 99 t.Fatalf("Bad Score: %v", out[0].FinalScore) 100 } 101 if out[1].FinalScore < 0.75 || out[1].FinalScore > 0.95 { 102 t.Fatalf("Bad Score: %v", out[1].FinalScore) 103 } 104 } 105 106 func TestBinPackIterator_PlannedAlloc(t *testing.T) { 107 _, ctx := testContext(t) 108 nodes := []*RankedNode{ 109 { 110 Node: &structs.Node{ 111 // Perfect fit 112 ID: uuid.Generate(), 113 Resources: &structs.Resources{ 114 CPU: 2048, 115 MemoryMB: 2048, 116 }, 117 }, 118 }, 119 { 120 Node: &structs.Node{ 121 // Perfect fit 122 ID: uuid.Generate(), 123 Resources: &structs.Resources{ 124 CPU: 2048, 125 MemoryMB: 2048, 126 }, 127 }, 128 }, 129 } 130 static := NewStaticRankIterator(ctx, nodes) 131 132 // Add a planned alloc to node1 that fills it 133 plan := ctx.Plan() 134 plan.NodeAllocation[nodes[0].Node.ID] = []*structs.Allocation{ 135 { 136 Resources: &structs.Resources{ 137 CPU: 2048, 138 MemoryMB: 2048, 139 }, 140 }, 141 } 142 143 // Add a planned alloc to node2 that half fills it 144 plan.NodeAllocation[nodes[1].Node.ID] = []*structs.Allocation{ 145 { 146 Resources: &structs.Resources{ 147 CPU: 1024, 148 MemoryMB: 1024, 149 }, 150 }, 151 } 152 153 taskGroup := &structs.TaskGroup{ 154 EphemeralDisk: &structs.EphemeralDisk{}, 155 Tasks: []*structs.Task{ 156 { 157 Name: "web", 158 Resources: &structs.Resources{ 159 CPU: 1024, 160 MemoryMB: 1024, 161 }, 162 }, 163 }, 164 } 165 166 binp := NewBinPackIterator(ctx, static, false, 0) 167 binp.SetTaskGroup(taskGroup) 168 169 scoreNorm := NewScoreNormalizationIterator(ctx, binp) 170 171 out := collectRanked(scoreNorm) 172 if len(out) != 1 { 173 t.Fatalf("Bad: %#v", out) 174 } 175 if out[0] != nodes[1] { 176 t.Fatalf("Bad Score: %v", out) 177 } 178 179 if out[0].FinalScore != 1.0 { 180 t.Fatalf("Bad Score: %v", out[0].FinalScore) 181 } 182 } 183 184 func TestBinPackIterator_ExistingAlloc(t *testing.T) { 185 state, ctx := testContext(t) 186 nodes := []*RankedNode{ 187 { 188 Node: &structs.Node{ 189 // Perfect fit 190 ID: uuid.Generate(), 191 Resources: &structs.Resources{ 192 CPU: 2048, 193 MemoryMB: 2048, 194 }, 195 }, 196 }, 197 { 198 Node: &structs.Node{ 199 // Perfect fit 200 ID: uuid.Generate(), 201 Resources: &structs.Resources{ 202 CPU: 2048, 203 MemoryMB: 2048, 204 }, 205 }, 206 }, 207 } 208 static := NewStaticRankIterator(ctx, nodes) 209 210 // Add existing allocations 211 j1, j2 := mock.Job(), mock.Job() 212 alloc1 := &structs.Allocation{ 213 Namespace: structs.DefaultNamespace, 214 ID: uuid.Generate(), 215 EvalID: uuid.Generate(), 216 NodeID: nodes[0].Node.ID, 217 JobID: j1.ID, 218 Job: j1, 219 Resources: &structs.Resources{ 220 CPU: 2048, 221 MemoryMB: 2048, 222 }, 223 DesiredStatus: structs.AllocDesiredStatusRun, 224 ClientStatus: structs.AllocClientStatusPending, 225 TaskGroup: "web", 226 } 227 alloc2 := &structs.Allocation{ 228 Namespace: structs.DefaultNamespace, 229 ID: uuid.Generate(), 230 EvalID: uuid.Generate(), 231 NodeID: nodes[1].Node.ID, 232 JobID: j2.ID, 233 Job: j2, 234 Resources: &structs.Resources{ 235 CPU: 1024, 236 MemoryMB: 1024, 237 }, 238 DesiredStatus: structs.AllocDesiredStatusRun, 239 ClientStatus: structs.AllocClientStatusPending, 240 TaskGroup: "web", 241 } 242 noErr(t, state.UpsertJobSummary(998, mock.JobSummary(alloc1.JobID))) 243 noErr(t, state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID))) 244 noErr(t, state.UpsertAllocs(1000, []*structs.Allocation{alloc1, alloc2})) 245 246 taskGroup := &structs.TaskGroup{ 247 EphemeralDisk: &structs.EphemeralDisk{}, 248 Tasks: []*structs.Task{ 249 { 250 Name: "web", 251 Resources: &structs.Resources{ 252 CPU: 1024, 253 MemoryMB: 1024, 254 }, 255 }, 256 }, 257 } 258 binp := NewBinPackIterator(ctx, static, false, 0) 259 binp.SetTaskGroup(taskGroup) 260 261 scoreNorm := NewScoreNormalizationIterator(ctx, binp) 262 263 out := collectRanked(scoreNorm) 264 if len(out) != 1 { 265 t.Fatalf("Bad: %#v", out) 266 } 267 if out[0] != nodes[1] { 268 t.Fatalf("Bad: %v", out) 269 } 270 if out[0].FinalScore != 1.0 { 271 t.Fatalf("Bad Score: %v", out[0].FinalScore) 272 } 273 } 274 275 func TestBinPackIterator_ExistingAlloc_PlannedEvict(t *testing.T) { 276 state, ctx := testContext(t) 277 nodes := []*RankedNode{ 278 { 279 Node: &structs.Node{ 280 // Perfect fit 281 ID: uuid.Generate(), 282 Resources: &structs.Resources{ 283 CPU: 2048, 284 MemoryMB: 2048, 285 }, 286 }, 287 }, 288 { 289 Node: &structs.Node{ 290 // Perfect fit 291 ID: uuid.Generate(), 292 Resources: &structs.Resources{ 293 CPU: 2048, 294 MemoryMB: 2048, 295 }, 296 }, 297 }, 298 } 299 static := NewStaticRankIterator(ctx, nodes) 300 301 // Add existing allocations 302 j1, j2 := mock.Job(), mock.Job() 303 alloc1 := &structs.Allocation{ 304 Namespace: structs.DefaultNamespace, 305 ID: uuid.Generate(), 306 EvalID: uuid.Generate(), 307 NodeID: nodes[0].Node.ID, 308 JobID: j1.ID, 309 Job: j1, 310 Resources: &structs.Resources{ 311 CPU: 2048, 312 MemoryMB: 2048, 313 }, 314 DesiredStatus: structs.AllocDesiredStatusRun, 315 ClientStatus: structs.AllocClientStatusPending, 316 TaskGroup: "web", 317 } 318 alloc2 := &structs.Allocation{ 319 Namespace: structs.DefaultNamespace, 320 ID: uuid.Generate(), 321 EvalID: uuid.Generate(), 322 NodeID: nodes[1].Node.ID, 323 JobID: j2.ID, 324 Job: j2, 325 Resources: &structs.Resources{ 326 CPU: 1024, 327 MemoryMB: 1024, 328 }, 329 DesiredStatus: structs.AllocDesiredStatusRun, 330 ClientStatus: structs.AllocClientStatusPending, 331 TaskGroup: "web", 332 } 333 noErr(t, state.UpsertJobSummary(998, mock.JobSummary(alloc1.JobID))) 334 noErr(t, state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID))) 335 noErr(t, state.UpsertAllocs(1000, []*structs.Allocation{alloc1, alloc2})) 336 337 // Add a planned eviction to alloc1 338 plan := ctx.Plan() 339 plan.NodeUpdate[nodes[0].Node.ID] = []*structs.Allocation{alloc1} 340 341 taskGroup := &structs.TaskGroup{ 342 EphemeralDisk: &structs.EphemeralDisk{}, 343 Tasks: []*structs.Task{ 344 { 345 Name: "web", 346 Resources: &structs.Resources{ 347 CPU: 1024, 348 MemoryMB: 1024, 349 }, 350 }, 351 }, 352 } 353 354 binp := NewBinPackIterator(ctx, static, false, 0) 355 binp.SetTaskGroup(taskGroup) 356 357 scoreNorm := NewScoreNormalizationIterator(ctx, binp) 358 359 out := collectRanked(scoreNorm) 360 if len(out) != 2 { 361 t.Fatalf("Bad: %#v", out) 362 } 363 if out[0] != nodes[0] || out[1] != nodes[1] { 364 t.Fatalf("Bad: %v", out) 365 } 366 if out[0].FinalScore < 0.50 || out[0].FinalScore > 0.95 { 367 t.Fatalf("Bad Score: %v", out[0].FinalScore) 368 } 369 if out[1].FinalScore != 1 { 370 t.Fatalf("Bad Score: %v", out[1].FinalScore) 371 } 372 } 373 374 func TestJobAntiAffinity_PlannedAlloc(t *testing.T) { 375 _, ctx := testContext(t) 376 nodes := []*RankedNode{ 377 { 378 Node: &structs.Node{ 379 ID: uuid.Generate(), 380 }, 381 }, 382 { 383 Node: &structs.Node{ 384 ID: uuid.Generate(), 385 }, 386 }, 387 } 388 static := NewStaticRankIterator(ctx, nodes) 389 390 job := mock.Job() 391 job.ID = "foo" 392 tg := job.TaskGroups[0] 393 tg.Count = 4 394 395 // Add a planned alloc to node1 that fills it 396 plan := ctx.Plan() 397 plan.NodeAllocation[nodes[0].Node.ID] = []*structs.Allocation{ 398 { 399 ID: uuid.Generate(), 400 JobID: "foo", 401 TaskGroup: tg.Name, 402 }, 403 { 404 ID: uuid.Generate(), 405 JobID: "foo", 406 TaskGroup: tg.Name, 407 }, 408 } 409 410 // Add a planned alloc to node2 that half fills it 411 plan.NodeAllocation[nodes[1].Node.ID] = []*structs.Allocation{ 412 { 413 JobID: "bar", 414 }, 415 } 416 417 jobAntiAff := NewJobAntiAffinityIterator(ctx, static, "foo") 418 jobAntiAff.SetJob(job) 419 jobAntiAff.SetTaskGroup(tg) 420 421 scoreNorm := NewScoreNormalizationIterator(ctx, jobAntiAff) 422 423 out := collectRanked(scoreNorm) 424 if len(out) != 2 { 425 t.Fatalf("Bad: %#v", out) 426 } 427 if out[0] != nodes[0] { 428 t.Fatalf("Bad: %v", out) 429 } 430 // Score should be -(#collissions+1/desired_count) => -(3/4) 431 if out[0].FinalScore != -0.75 { 432 t.Fatalf("Bad Score: %#v", out[0].FinalScore) 433 } 434 435 if out[1] != nodes[1] { 436 t.Fatalf("Bad: %v", out) 437 } 438 if out[1].FinalScore != 0.0 { 439 t.Fatalf("Bad Score: %v", out[1].FinalScore) 440 } 441 } 442 443 func collectRanked(iter RankIterator) (out []*RankedNode) { 444 for { 445 next := iter.Next() 446 if next == nil { 447 break 448 } 449 out = append(out, next) 450 } 451 return 452 } 453 454 func TestNodeAntiAffinity_PenaltyNodes(t *testing.T) { 455 _, ctx := testContext(t) 456 node1 := &structs.Node{ 457 ID: uuid.Generate(), 458 } 459 node2 := &structs.Node{ 460 ID: uuid.Generate(), 461 } 462 463 nodes := []*RankedNode{ 464 { 465 Node: node1, 466 }, 467 { 468 Node: node2, 469 }, 470 } 471 static := NewStaticRankIterator(ctx, nodes) 472 473 nodeAntiAffIter := NewNodeReschedulingPenaltyIterator(ctx, static) 474 nodeAntiAffIter.SetPenaltyNodes(map[string]struct{}{node1.ID: {}}) 475 476 scoreNorm := NewScoreNormalizationIterator(ctx, nodeAntiAffIter) 477 478 out := collectRanked(scoreNorm) 479 480 require := require.New(t) 481 require.Equal(2, len(out)) 482 require.Equal(node1.ID, out[0].Node.ID) 483 require.Equal(-1.0, out[0].FinalScore) 484 485 require.Equal(node2.ID, out[1].Node.ID) 486 require.Equal(0.0, out[1].FinalScore) 487 488 } 489 490 func TestScoreNormalizationIterator(t *testing.T) { 491 // Test normalized scores when there is more than one scorer 492 _, ctx := testContext(t) 493 nodes := []*RankedNode{ 494 { 495 Node: &structs.Node{ 496 ID: uuid.Generate(), 497 }, 498 }, 499 { 500 Node: &structs.Node{ 501 ID: uuid.Generate(), 502 }, 503 }, 504 } 505 static := NewStaticRankIterator(ctx, nodes) 506 507 job := mock.Job() 508 job.ID = "foo" 509 tg := job.TaskGroups[0] 510 tg.Count = 4 511 512 // Add a planned alloc to node1 that fills it 513 plan := ctx.Plan() 514 plan.NodeAllocation[nodes[0].Node.ID] = []*structs.Allocation{ 515 { 516 ID: uuid.Generate(), 517 JobID: "foo", 518 TaskGroup: tg.Name, 519 }, 520 { 521 ID: uuid.Generate(), 522 JobID: "foo", 523 TaskGroup: tg.Name, 524 }, 525 } 526 527 // Add a planned alloc to node2 that half fills it 528 plan.NodeAllocation[nodes[1].Node.ID] = []*structs.Allocation{ 529 { 530 JobID: "bar", 531 }, 532 } 533 534 jobAntiAff := NewJobAntiAffinityIterator(ctx, static, "foo") 535 jobAntiAff.SetJob(job) 536 jobAntiAff.SetTaskGroup(tg) 537 538 nodeReschedulePenaltyIter := NewNodeReschedulingPenaltyIterator(ctx, jobAntiAff) 539 nodeReschedulePenaltyIter.SetPenaltyNodes(map[string]struct{}{nodes[0].Node.ID: {}}) 540 541 scoreNorm := NewScoreNormalizationIterator(ctx, nodeReschedulePenaltyIter) 542 543 out := collectRanked(scoreNorm) 544 require := require.New(t) 545 546 require.Equal(2, len(out)) 547 require.Equal(out[0], nodes[0]) 548 // Score should be averaged between both scorers 549 // -0.75 from job anti affinity and -1 from node rescheduling penalty 550 require.Equal(-0.875, out[0].FinalScore) 551 require.Equal(out[1], nodes[1]) 552 require.Equal(out[1].FinalScore, 0.0) 553 } 554 555 func TestNodeAffinityIterator(t *testing.T) { 556 _, ctx := testContext(t) 557 nodes := []*RankedNode{ 558 {Node: mock.Node()}, 559 {Node: mock.Node()}, 560 {Node: mock.Node()}, 561 {Node: mock.Node()}, 562 } 563 564 nodes[0].Node.Attributes["kernel.version"] = "4.9" 565 nodes[1].Node.Datacenter = "dc2" 566 nodes[2].Node.Datacenter = "dc2" 567 nodes[2].Node.NodeClass = "large" 568 569 affinities := []*structs.Affinity{ 570 { 571 Operand: "=", 572 LTarget: "${node.datacenter}", 573 RTarget: "dc1", 574 Weight: 200, 575 }, 576 { 577 Operand: "=", 578 LTarget: "${node.datacenter}", 579 RTarget: "dc2", 580 Weight: -100, 581 }, 582 { 583 Operand: "version", 584 LTarget: "${attr.kernel.version}", 585 RTarget: ">4.0", 586 Weight: 50, 587 }, 588 { 589 Operand: "is", 590 LTarget: "${node.class}", 591 RTarget: "large", 592 Weight: 50, 593 }, 594 } 595 596 static := NewStaticRankIterator(ctx, nodes) 597 598 job := mock.Job() 599 job.ID = "foo" 600 tg := job.TaskGroups[0] 601 tg.Affinities = affinities 602 603 nodeAffinity := NewNodeAffinityIterator(ctx, static) 604 nodeAffinity.SetTaskGroup(tg) 605 606 scoreNorm := NewScoreNormalizationIterator(ctx, nodeAffinity) 607 608 out := collectRanked(scoreNorm) 609 expectedScores := make(map[string]float64) 610 // Total weight = 400 611 // Node 0 matches two affinities(dc and kernel version), total weight =250 612 expectedScores[nodes[0].Node.ID] = 0.625 613 614 // Node 1 matches an anti affinity, weight = -100 615 expectedScores[nodes[1].Node.ID] = -0.25 616 617 // Node 2 matches one affinity(node class) with weight 50 618 expectedScores[nodes[2].Node.ID] = -0.125 619 620 // Node 3 matches one affinity (dc) with weight = 200 621 expectedScores[nodes[3].Node.ID] = 0.5 622 623 require := require.New(t) 624 for _, n := range out { 625 require.Equal(expectedScores[n.Node.ID], n.FinalScore) 626 } 627 628 }