github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/scheduler/stack_test.go (about) 1 package scheduler 2 3 import ( 4 "fmt" 5 "reflect" 6 "runtime" 7 "testing" 8 9 "github.com/hashicorp/nomad/nomad/mock" 10 "github.com/hashicorp/nomad/nomad/structs" 11 "github.com/stretchr/testify/require" 12 ) 13 14 func BenchmarkServiceStack_With_ComputedClass(b *testing.B) { 15 // Key doesn't escape computed node class. 16 benchmarkServiceStack_MetaKeyConstraint(b, "key", 5000, 64) 17 } 18 19 func BenchmarkServiceStack_WithOut_ComputedClass(b *testing.B) { 20 // Key escapes computed node class. 21 benchmarkServiceStack_MetaKeyConstraint(b, "unique.key", 5000, 64) 22 } 23 24 // benchmarkServiceStack_MetaKeyConstraint creates the passed number of nodes 25 // and sets the meta data key to have nodePartitions number of values. It then 26 // benchmarks the stack by selecting a job that constrains against one of the 27 // partitions. 28 func benchmarkServiceStack_MetaKeyConstraint(b *testing.B, key string, numNodes, nodePartitions int) { 29 _, ctx := testContext(b) 30 stack := NewGenericStack(false, ctx) 31 32 // Create 4 classes of nodes. 33 nodes := make([]*structs.Node, numNodes) 34 for i := 0; i < numNodes; i++ { 35 n := mock.Node() 36 n.Meta[key] = fmt.Sprintf("%d", i%nodePartitions) 37 nodes[i] = n 38 } 39 stack.SetNodes(nodes) 40 41 // Create a job whose constraint meets two node classes. 42 job := mock.Job() 43 job.Constraints[0] = &structs.Constraint{ 44 LTarget: fmt.Sprintf("${meta.%v}", key), 45 RTarget: "1", 46 Operand: "<", 47 } 48 stack.SetJob(job) 49 50 b.ResetTimer() 51 selectOptions := &SelectOptions{} 52 for i := 0; i < b.N; i++ { 53 stack.Select(job.TaskGroups[0], selectOptions) 54 } 55 } 56 57 func TestServiceStack_SetNodes(t *testing.T) { 58 _, ctx := testContext(t) 59 stack := NewGenericStack(false, ctx) 60 61 nodes := []*structs.Node{ 62 mock.Node(), 63 mock.Node(), 64 mock.Node(), 65 mock.Node(), 66 mock.Node(), 67 mock.Node(), 68 mock.Node(), 69 mock.Node(), 70 } 71 stack.SetNodes(nodes) 72 73 // Check that our scan limit is updated 74 if stack.limit.limit != 3 { 75 t.Fatalf("bad limit %d", stack.limit.limit) 76 } 77 78 out := collectFeasible(stack.source) 79 if !reflect.DeepEqual(out, nodes) { 80 t.Fatalf("bad: %#v", out) 81 } 82 } 83 84 func TestServiceStack_SetJob(t *testing.T) { 85 _, ctx := testContext(t) 86 stack := NewGenericStack(false, ctx) 87 88 job := mock.Job() 89 stack.SetJob(job) 90 91 if stack.binPack.priority != job.Priority { 92 t.Fatalf("bad") 93 } 94 if !reflect.DeepEqual(stack.jobConstraint.constraints, job.Constraints) { 95 t.Fatalf("bad") 96 } 97 } 98 99 func TestServiceStack_Select_Size(t *testing.T) { 100 _, ctx := testContext(t) 101 nodes := []*structs.Node{ 102 mock.Node(), 103 } 104 stack := NewGenericStack(false, ctx) 105 stack.SetNodes(nodes) 106 107 job := mock.Job() 108 stack.SetJob(job) 109 selectOptions := &SelectOptions{} 110 node, size := stack.Select(job.TaskGroups[0], selectOptions) 111 if node == nil { 112 t.Fatalf("missing node %#v", ctx.Metrics()) 113 } 114 if size == nil { 115 t.Fatalf("missing size") 116 } 117 118 if size.CPU != 500 || size.MemoryMB != 256 { 119 t.Fatalf("bad: %#v", size) 120 } 121 122 // Note: On Windows time.Now currently has a best case granularity of 1ms. 123 // We skip the following assertion on Windows because this test usually 124 // runs too fast to measure an allocation time on Windows. 125 met := ctx.Metrics() 126 if runtime.GOOS != "windows" && met.AllocationTime == 0 { 127 t.Fatalf("missing time") 128 } 129 } 130 131 func TestServiceStack_Select_PreferringNodes(t *testing.T) { 132 _, ctx := testContext(t) 133 nodes := []*structs.Node{ 134 mock.Node(), 135 } 136 stack := NewGenericStack(false, ctx) 137 stack.SetNodes(nodes) 138 139 job := mock.Job() 140 stack.SetJob(job) 141 142 // Create a preferred node 143 preferredNode := mock.Node() 144 prefNodes := []*structs.Node{preferredNode} 145 selectOptions := &SelectOptions{PreferredNodes: prefNodes} 146 option, _ := stack.Select(job.TaskGroups[0], selectOptions) 147 if option == nil { 148 t.Fatalf("missing node %#v", ctx.Metrics()) 149 } 150 if option.Node.ID != preferredNode.ID { 151 t.Fatalf("expected: %v, actual: %v", option.Node.ID, preferredNode.ID) 152 } 153 154 // Make sure select doesn't have a side effect on preferred nodes 155 require.Equal(t, prefNodes, selectOptions.PreferredNodes) 156 157 // Change the preferred node's kernel to windows and ensure the allocations 158 // are placed elsewhere 159 preferredNode1 := preferredNode.Copy() 160 preferredNode1.Attributes["kernel.name"] = "windows" 161 preferredNode1.ComputeClass() 162 prefNodes1 := []*structs.Node{preferredNode1} 163 selectOptions = &SelectOptions{PreferredNodes: prefNodes1} 164 option, _ = stack.Select(job.TaskGroups[0], selectOptions) 165 if option == nil { 166 t.Fatalf("missing node %#v", ctx.Metrics()) 167 } 168 169 if option.Node.ID != nodes[0].ID { 170 t.Fatalf("expected: %#v, actual: %#v", nodes[0], option.Node) 171 } 172 require.Equal(t, prefNodes1, selectOptions.PreferredNodes) 173 } 174 175 func TestServiceStack_Select_MetricsReset(t *testing.T) { 176 _, ctx := testContext(t) 177 nodes := []*structs.Node{ 178 mock.Node(), 179 mock.Node(), 180 mock.Node(), 181 mock.Node(), 182 } 183 stack := NewGenericStack(false, ctx) 184 stack.SetNodes(nodes) 185 186 job := mock.Job() 187 stack.SetJob(job) 188 selectOptions := &SelectOptions{} 189 n1, _ := stack.Select(job.TaskGroups[0], selectOptions) 190 m1 := ctx.Metrics() 191 if n1 == nil { 192 t.Fatalf("missing node %#v", m1) 193 } 194 195 if m1.NodesEvaluated != 2 { 196 t.Fatalf("should only be 2") 197 } 198 199 n2, _ := stack.Select(job.TaskGroups[0], selectOptions) 200 m2 := ctx.Metrics() 201 if n2 == nil { 202 t.Fatalf("missing node %#v", m2) 203 } 204 205 // If we don't reset, this would be 4 206 if m2.NodesEvaluated != 2 { 207 t.Fatalf("should only be 2") 208 } 209 } 210 211 func TestServiceStack_Select_DriverFilter(t *testing.T) { 212 _, ctx := testContext(t) 213 nodes := []*structs.Node{ 214 mock.Node(), 215 mock.Node(), 216 } 217 zero := nodes[0] 218 zero.Attributes["driver.foo"] = "1" 219 if err := zero.ComputeClass(); err != nil { 220 t.Fatalf("ComputedClass() failed: %v", err) 221 } 222 223 stack := NewGenericStack(false, ctx) 224 stack.SetNodes(nodes) 225 226 job := mock.Job() 227 job.TaskGroups[0].Tasks[0].Driver = "foo" 228 stack.SetJob(job) 229 230 selectOptions := &SelectOptions{} 231 node, _ := stack.Select(job.TaskGroups[0], selectOptions) 232 if node == nil { 233 t.Fatalf("missing node %#v", ctx.Metrics()) 234 } 235 236 if node.Node != zero { 237 t.Fatalf("bad") 238 } 239 } 240 241 func TestServiceStack_Select_ConstraintFilter(t *testing.T) { 242 _, ctx := testContext(t) 243 nodes := []*structs.Node{ 244 mock.Node(), 245 mock.Node(), 246 } 247 zero := nodes[0] 248 zero.Attributes["kernel.name"] = "freebsd" 249 if err := zero.ComputeClass(); err != nil { 250 t.Fatalf("ComputedClass() failed: %v", err) 251 } 252 253 stack := NewGenericStack(false, ctx) 254 stack.SetNodes(nodes) 255 256 job := mock.Job() 257 job.Constraints[0].RTarget = "freebsd" 258 stack.SetJob(job) 259 selectOptions := &SelectOptions{} 260 node, _ := stack.Select(job.TaskGroups[0], selectOptions) 261 if node == nil { 262 t.Fatalf("missing node %#v", ctx.Metrics()) 263 } 264 265 if node.Node != zero { 266 t.Fatalf("bad") 267 } 268 269 met := ctx.Metrics() 270 if met.NodesFiltered != 1 { 271 t.Fatalf("bad: %#v", met) 272 } 273 if met.ClassFiltered["linux-medium-pci"] != 1 { 274 t.Fatalf("bad: %#v", met) 275 } 276 if met.ConstraintFiltered["${attr.kernel.name} = freebsd"] != 1 { 277 t.Fatalf("bad: %#v", met) 278 } 279 } 280 281 func TestServiceStack_Select_BinPack_Overflow(t *testing.T) { 282 _, ctx := testContext(t) 283 nodes := []*structs.Node{ 284 mock.Node(), 285 mock.Node(), 286 } 287 zero := nodes[0] 288 one := nodes[1] 289 one.Reserved = one.Resources 290 291 stack := NewGenericStack(false, ctx) 292 stack.SetNodes(nodes) 293 294 job := mock.Job() 295 stack.SetJob(job) 296 selectOptions := &SelectOptions{} 297 node, _ := stack.Select(job.TaskGroups[0], selectOptions) 298 ctx.Metrics().PopulateScoreMetaData() 299 if node == nil { 300 t.Fatalf("missing node %#v", ctx.Metrics()) 301 } 302 303 if node.Node != zero { 304 t.Fatalf("bad") 305 } 306 307 met := ctx.Metrics() 308 if met.NodesExhausted != 1 { 309 t.Fatalf("bad: %#v", met) 310 } 311 if met.ClassExhausted["linux-medium-pci"] != 1 { 312 t.Fatalf("bad: %#v", met) 313 } 314 // Expect score metadata for one node 315 if len(met.ScoreMetaData) != 1 { 316 t.Fatalf("bad: %#v", met) 317 } 318 } 319 320 func TestSystemStack_SetNodes(t *testing.T) { 321 _, ctx := testContext(t) 322 stack := NewSystemStack(ctx) 323 324 nodes := []*structs.Node{ 325 mock.Node(), 326 mock.Node(), 327 mock.Node(), 328 mock.Node(), 329 mock.Node(), 330 mock.Node(), 331 mock.Node(), 332 mock.Node(), 333 } 334 stack.SetNodes(nodes) 335 336 out := collectFeasible(stack.source) 337 if !reflect.DeepEqual(out, nodes) { 338 t.Fatalf("bad: %#v", out) 339 } 340 } 341 342 func TestSystemStack_SetJob(t *testing.T) { 343 _, ctx := testContext(t) 344 stack := NewSystemStack(ctx) 345 346 job := mock.Job() 347 stack.SetJob(job) 348 349 if stack.binPack.priority != job.Priority { 350 t.Fatalf("bad") 351 } 352 if !reflect.DeepEqual(stack.jobConstraint.constraints, job.Constraints) { 353 t.Fatalf("bad") 354 } 355 } 356 357 func TestSystemStack_Select_Size(t *testing.T) { 358 _, ctx := testContext(t) 359 nodes := []*structs.Node{mock.Node()} 360 stack := NewSystemStack(ctx) 361 stack.SetNodes(nodes) 362 363 job := mock.Job() 364 stack.SetJob(job) 365 selectOptions := &SelectOptions{} 366 node, size := stack.Select(job.TaskGroups[0], selectOptions) 367 if node == nil { 368 t.Fatalf("missing node %#v", ctx.Metrics()) 369 } 370 if size == nil { 371 t.Fatalf("missing size") 372 } 373 374 if size.CPU != 500 || size.MemoryMB != 256 { 375 t.Fatalf("bad: %#v", size) 376 } 377 378 // Note: On Windows time.Now currently has a best case granularity of 1ms. 379 // We skip the following assertion on Windows because this test usually 380 // runs too fast to measure an allocation time on Windows. 381 met := ctx.Metrics() 382 if runtime.GOOS != "windows" && met.AllocationTime == 0 { 383 t.Fatalf("missing time") 384 } 385 } 386 387 func TestSystemStack_Select_MetricsReset(t *testing.T) { 388 _, ctx := testContext(t) 389 nodes := []*structs.Node{ 390 mock.Node(), 391 mock.Node(), 392 mock.Node(), 393 mock.Node(), 394 } 395 stack := NewSystemStack(ctx) 396 stack.SetNodes(nodes) 397 398 job := mock.Job() 399 stack.SetJob(job) 400 selectOptions := &SelectOptions{} 401 n1, _ := stack.Select(job.TaskGroups[0], selectOptions) 402 m1 := ctx.Metrics() 403 if n1 == nil { 404 t.Fatalf("missing node %#v", m1) 405 } 406 407 if m1.NodesEvaluated != 1 { 408 t.Fatalf("should only be 1") 409 } 410 411 n2, _ := stack.Select(job.TaskGroups[0], selectOptions) 412 m2 := ctx.Metrics() 413 if n2 == nil { 414 t.Fatalf("missing node %#v", m2) 415 } 416 417 // If we don't reset, this would be 2 418 if m2.NodesEvaluated != 1 { 419 t.Fatalf("should only be 2") 420 } 421 } 422 423 func TestSystemStack_Select_DriverFilter(t *testing.T) { 424 _, ctx := testContext(t) 425 nodes := []*structs.Node{ 426 mock.Node(), 427 } 428 zero := nodes[0] 429 zero.Attributes["driver.foo"] = "1" 430 431 stack := NewSystemStack(ctx) 432 stack.SetNodes(nodes) 433 434 job := mock.Job() 435 job.TaskGroups[0].Tasks[0].Driver = "foo" 436 stack.SetJob(job) 437 438 selectOptions := &SelectOptions{} 439 node, _ := stack.Select(job.TaskGroups[0], selectOptions) 440 if node == nil { 441 t.Fatalf("missing node %#v", ctx.Metrics()) 442 } 443 444 if node.Node != zero { 445 t.Fatalf("bad") 446 } 447 448 zero.Attributes["driver.foo"] = "0" 449 if err := zero.ComputeClass(); err != nil { 450 t.Fatalf("ComputedClass() failed: %v", err) 451 } 452 453 stack = NewSystemStack(ctx) 454 stack.SetNodes(nodes) 455 stack.SetJob(job) 456 node, _ = stack.Select(job.TaskGroups[0], selectOptions) 457 if node != nil { 458 t.Fatalf("node not filtered %#v", node) 459 } 460 } 461 462 func TestSystemStack_Select_ConstraintFilter(t *testing.T) { 463 _, ctx := testContext(t) 464 nodes := []*structs.Node{ 465 mock.Node(), 466 mock.Node(), 467 } 468 zero := nodes[1] 469 zero.Attributes["kernel.name"] = "freebsd" 470 if err := zero.ComputeClass(); err != nil { 471 t.Fatalf("ComputedClass() failed: %v", err) 472 } 473 474 stack := NewSystemStack(ctx) 475 stack.SetNodes(nodes) 476 477 job := mock.Job() 478 job.Constraints[0].RTarget = "freebsd" 479 stack.SetJob(job) 480 481 selectOptions := &SelectOptions{} 482 node, _ := stack.Select(job.TaskGroups[0], selectOptions) 483 if node == nil { 484 t.Fatalf("missing node %#v", ctx.Metrics()) 485 } 486 487 if node.Node != zero { 488 t.Fatalf("bad") 489 } 490 491 met := ctx.Metrics() 492 if met.NodesFiltered != 1 { 493 t.Fatalf("bad: %#v", met) 494 } 495 if met.ClassFiltered["linux-medium-pci"] != 1 { 496 t.Fatalf("bad: %#v", met) 497 } 498 if met.ConstraintFiltered["${attr.kernel.name} = freebsd"] != 1 { 499 t.Fatalf("bad: %#v", met) 500 } 501 } 502 503 func TestSystemStack_Select_BinPack_Overflow(t *testing.T) { 504 _, ctx := testContext(t) 505 nodes := []*structs.Node{ 506 mock.Node(), 507 mock.Node(), 508 } 509 zero := nodes[0] 510 zero.Reserved = zero.Resources 511 one := nodes[1] 512 513 stack := NewSystemStack(ctx) 514 stack.SetNodes(nodes) 515 516 job := mock.Job() 517 stack.SetJob(job) 518 519 selectOptions := &SelectOptions{} 520 node, _ := stack.Select(job.TaskGroups[0], selectOptions) 521 ctx.Metrics().PopulateScoreMetaData() 522 if node == nil { 523 t.Fatalf("missing node %#v", ctx.Metrics()) 524 } 525 526 if node.Node != one { 527 t.Fatalf("bad") 528 } 529 530 met := ctx.Metrics() 531 if met.NodesExhausted != 1 { 532 t.Fatalf("bad: %#v", met) 533 } 534 if met.ClassExhausted["linux-medium-pci"] != 1 { 535 t.Fatalf("bad: %#v", met) 536 } 537 // Should have two scores, one from bin packing and one from normalization 538 if len(met.ScoreMetaData) != 1 { 539 t.Fatalf("bad: %#v", met) 540 } 541 }