github.com/bigcommerce/nomad@v0.9.3-bc/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 := stack.Select(job.TaskGroups[0], selectOptions) 111 if node == nil { 112 t.Fatalf("missing node %#v", ctx.Metrics()) 113 } 114 115 // Note: On Windows time.Now currently has a best case granularity of 1ms. 116 // We skip the following assertion on Windows because this test usually 117 // runs too fast to measure an allocation time on Windows. 118 met := ctx.Metrics() 119 if runtime.GOOS != "windows" && met.AllocationTime == 0 { 120 t.Fatalf("missing time") 121 } 122 } 123 124 func TestServiceStack_Select_PreferringNodes(t *testing.T) { 125 _, ctx := testContext(t) 126 nodes := []*structs.Node{ 127 mock.Node(), 128 } 129 stack := NewGenericStack(false, ctx) 130 stack.SetNodes(nodes) 131 132 job := mock.Job() 133 stack.SetJob(job) 134 135 // Create a preferred node 136 preferredNode := mock.Node() 137 prefNodes := []*structs.Node{preferredNode} 138 selectOptions := &SelectOptions{PreferredNodes: prefNodes} 139 option := stack.Select(job.TaskGroups[0], selectOptions) 140 if option == nil { 141 t.Fatalf("missing node %#v", ctx.Metrics()) 142 } 143 if option.Node.ID != preferredNode.ID { 144 t.Fatalf("expected: %v, actual: %v", option.Node.ID, preferredNode.ID) 145 } 146 147 // Make sure select doesn't have a side effect on preferred nodes 148 require.Equal(t, prefNodes, selectOptions.PreferredNodes) 149 150 // Change the preferred node's kernel to windows and ensure the allocations 151 // are placed elsewhere 152 preferredNode1 := preferredNode.Copy() 153 preferredNode1.Attributes["kernel.name"] = "windows" 154 preferredNode1.ComputeClass() 155 prefNodes1 := []*structs.Node{preferredNode1} 156 selectOptions = &SelectOptions{PreferredNodes: prefNodes1} 157 option = stack.Select(job.TaskGroups[0], selectOptions) 158 if option == nil { 159 t.Fatalf("missing node %#v", ctx.Metrics()) 160 } 161 162 if option.Node.ID != nodes[0].ID { 163 t.Fatalf("expected: %#v, actual: %#v", nodes[0], option.Node) 164 } 165 require.Equal(t, prefNodes1, selectOptions.PreferredNodes) 166 } 167 168 func TestServiceStack_Select_MetricsReset(t *testing.T) { 169 _, ctx := testContext(t) 170 nodes := []*structs.Node{ 171 mock.Node(), 172 mock.Node(), 173 mock.Node(), 174 mock.Node(), 175 } 176 stack := NewGenericStack(false, ctx) 177 stack.SetNodes(nodes) 178 179 job := mock.Job() 180 stack.SetJob(job) 181 selectOptions := &SelectOptions{} 182 n1 := stack.Select(job.TaskGroups[0], selectOptions) 183 m1 := ctx.Metrics() 184 if n1 == nil { 185 t.Fatalf("missing node %#v", m1) 186 } 187 188 if m1.NodesEvaluated != 2 { 189 t.Fatalf("should only be 2") 190 } 191 192 n2 := stack.Select(job.TaskGroups[0], selectOptions) 193 m2 := ctx.Metrics() 194 if n2 == nil { 195 t.Fatalf("missing node %#v", m2) 196 } 197 198 // If we don't reset, this would be 4 199 if m2.NodesEvaluated != 2 { 200 t.Fatalf("should only be 2") 201 } 202 } 203 204 func TestServiceStack_Select_DriverFilter(t *testing.T) { 205 _, ctx := testContext(t) 206 nodes := []*structs.Node{ 207 mock.Node(), 208 mock.Node(), 209 } 210 zero := nodes[0] 211 zero.Attributes["driver.foo"] = "1" 212 if err := zero.ComputeClass(); err != nil { 213 t.Fatalf("ComputedClass() failed: %v", err) 214 } 215 216 stack := NewGenericStack(false, ctx) 217 stack.SetNodes(nodes) 218 219 job := mock.Job() 220 job.TaskGroups[0].Tasks[0].Driver = "foo" 221 stack.SetJob(job) 222 223 selectOptions := &SelectOptions{} 224 node := stack.Select(job.TaskGroups[0], selectOptions) 225 if node == nil { 226 t.Fatalf("missing node %#v", ctx.Metrics()) 227 } 228 229 if node.Node != zero { 230 t.Fatalf("bad") 231 } 232 } 233 234 func TestServiceStack_Select_ConstraintFilter(t *testing.T) { 235 _, ctx := testContext(t) 236 nodes := []*structs.Node{ 237 mock.Node(), 238 mock.Node(), 239 } 240 zero := nodes[0] 241 zero.Attributes["kernel.name"] = "freebsd" 242 if err := zero.ComputeClass(); err != nil { 243 t.Fatalf("ComputedClass() failed: %v", err) 244 } 245 246 stack := NewGenericStack(false, ctx) 247 stack.SetNodes(nodes) 248 249 job := mock.Job() 250 job.Constraints[0].RTarget = "freebsd" 251 stack.SetJob(job) 252 selectOptions := &SelectOptions{} 253 node := stack.Select(job.TaskGroups[0], selectOptions) 254 if node == nil { 255 t.Fatalf("missing node %#v", ctx.Metrics()) 256 } 257 258 if node.Node != zero { 259 t.Fatalf("bad") 260 } 261 262 met := ctx.Metrics() 263 if met.NodesFiltered != 1 { 264 t.Fatalf("bad: %#v", met) 265 } 266 if met.ClassFiltered["linux-medium-pci"] != 1 { 267 t.Fatalf("bad: %#v", met) 268 } 269 if met.ConstraintFiltered["${attr.kernel.name} = freebsd"] != 1 { 270 t.Fatalf("bad: %#v", met) 271 } 272 } 273 274 func TestServiceStack_Select_BinPack_Overflow(t *testing.T) { 275 _, ctx := testContext(t) 276 nodes := []*structs.Node{ 277 mock.Node(), 278 mock.Node(), 279 } 280 zero := nodes[0] 281 one := nodes[1] 282 one.ReservedResources = &structs.NodeReservedResources{ 283 Cpu: structs.NodeReservedCpuResources{ 284 CpuShares: one.NodeResources.Cpu.CpuShares, 285 }, 286 } 287 288 stack := NewGenericStack(false, ctx) 289 stack.SetNodes(nodes) 290 291 job := mock.Job() 292 stack.SetJob(job) 293 selectOptions := &SelectOptions{} 294 node := stack.Select(job.TaskGroups[0], selectOptions) 295 ctx.Metrics().PopulateScoreMetaData() 296 if node == nil { 297 t.Fatalf("missing node %#v", ctx.Metrics()) 298 } 299 300 if node.Node != zero { 301 t.Fatalf("bad") 302 } 303 304 met := ctx.Metrics() 305 if met.NodesExhausted != 1 { 306 t.Fatalf("bad: %#v", met) 307 } 308 if met.ClassExhausted["linux-medium-pci"] != 1 { 309 t.Fatalf("bad: %#v", met) 310 } 311 // Expect score metadata for one node 312 if len(met.ScoreMetaData) != 1 { 313 t.Fatalf("bad: %#v", met) 314 } 315 } 316 317 func TestSystemStack_SetNodes(t *testing.T) { 318 _, ctx := testContext(t) 319 stack := NewSystemStack(ctx) 320 321 nodes := []*structs.Node{ 322 mock.Node(), 323 mock.Node(), 324 mock.Node(), 325 mock.Node(), 326 mock.Node(), 327 mock.Node(), 328 mock.Node(), 329 mock.Node(), 330 } 331 stack.SetNodes(nodes) 332 333 out := collectFeasible(stack.source) 334 if !reflect.DeepEqual(out, nodes) { 335 t.Fatalf("bad: %#v", out) 336 } 337 } 338 339 func TestSystemStack_SetJob(t *testing.T) { 340 _, ctx := testContext(t) 341 stack := NewSystemStack(ctx) 342 343 job := mock.Job() 344 stack.SetJob(job) 345 346 if stack.binPack.priority != job.Priority { 347 t.Fatalf("bad") 348 } 349 if !reflect.DeepEqual(stack.jobConstraint.constraints, job.Constraints) { 350 t.Fatalf("bad") 351 } 352 } 353 354 func TestSystemStack_Select_Size(t *testing.T) { 355 _, ctx := testContext(t) 356 nodes := []*structs.Node{mock.Node()} 357 stack := NewSystemStack(ctx) 358 stack.SetNodes(nodes) 359 360 job := mock.Job() 361 stack.SetJob(job) 362 selectOptions := &SelectOptions{} 363 node := stack.Select(job.TaskGroups[0], selectOptions) 364 if node == nil { 365 t.Fatalf("missing node %#v", ctx.Metrics()) 366 } 367 368 // Note: On Windows time.Now currently has a best case granularity of 1ms. 369 // We skip the following assertion on Windows because this test usually 370 // runs too fast to measure an allocation time on Windows. 371 met := ctx.Metrics() 372 if runtime.GOOS != "windows" && met.AllocationTime == 0 { 373 t.Fatalf("missing time") 374 } 375 } 376 377 func TestSystemStack_Select_MetricsReset(t *testing.T) { 378 _, ctx := testContext(t) 379 nodes := []*structs.Node{ 380 mock.Node(), 381 mock.Node(), 382 mock.Node(), 383 mock.Node(), 384 } 385 stack := NewSystemStack(ctx) 386 stack.SetNodes(nodes) 387 388 job := mock.Job() 389 stack.SetJob(job) 390 selectOptions := &SelectOptions{} 391 n1 := stack.Select(job.TaskGroups[0], selectOptions) 392 m1 := ctx.Metrics() 393 if n1 == nil { 394 t.Fatalf("missing node %#v", m1) 395 } 396 397 if m1.NodesEvaluated != 1 { 398 t.Fatalf("should only be 1") 399 } 400 401 n2 := stack.Select(job.TaskGroups[0], selectOptions) 402 m2 := ctx.Metrics() 403 if n2 == nil { 404 t.Fatalf("missing node %#v", m2) 405 } 406 407 // If we don't reset, this would be 2 408 if m2.NodesEvaluated != 1 { 409 t.Fatalf("should only be 2") 410 } 411 } 412 413 func TestSystemStack_Select_DriverFilter(t *testing.T) { 414 _, ctx := testContext(t) 415 nodes := []*structs.Node{ 416 mock.Node(), 417 } 418 zero := nodes[0] 419 zero.Attributes["driver.foo"] = "1" 420 421 stack := NewSystemStack(ctx) 422 stack.SetNodes(nodes) 423 424 job := mock.Job() 425 job.TaskGroups[0].Tasks[0].Driver = "foo" 426 stack.SetJob(job) 427 428 selectOptions := &SelectOptions{} 429 node := stack.Select(job.TaskGroups[0], selectOptions) 430 if node == nil { 431 t.Fatalf("missing node %#v", ctx.Metrics()) 432 } 433 434 if node.Node != zero { 435 t.Fatalf("bad") 436 } 437 438 zero.Attributes["driver.foo"] = "0" 439 if err := zero.ComputeClass(); err != nil { 440 t.Fatalf("ComputedClass() failed: %v", err) 441 } 442 443 stack = NewSystemStack(ctx) 444 stack.SetNodes(nodes) 445 stack.SetJob(job) 446 node = stack.Select(job.TaskGroups[0], selectOptions) 447 if node != nil { 448 t.Fatalf("node not filtered %#v", node) 449 } 450 } 451 452 func TestSystemStack_Select_ConstraintFilter(t *testing.T) { 453 _, ctx := testContext(t) 454 nodes := []*structs.Node{ 455 mock.Node(), 456 mock.Node(), 457 } 458 zero := nodes[1] 459 zero.Attributes["kernel.name"] = "freebsd" 460 if err := zero.ComputeClass(); err != nil { 461 t.Fatalf("ComputedClass() failed: %v", err) 462 } 463 464 stack := NewSystemStack(ctx) 465 stack.SetNodes(nodes) 466 467 job := mock.Job() 468 job.Constraints[0].RTarget = "freebsd" 469 stack.SetJob(job) 470 471 selectOptions := &SelectOptions{} 472 node := stack.Select(job.TaskGroups[0], selectOptions) 473 if node == nil { 474 t.Fatalf("missing node %#v", ctx.Metrics()) 475 } 476 477 if node.Node != zero { 478 t.Fatalf("bad") 479 } 480 481 met := ctx.Metrics() 482 if met.NodesFiltered != 1 { 483 t.Fatalf("bad: %#v", met) 484 } 485 if met.ClassFiltered["linux-medium-pci"] != 1 { 486 t.Fatalf("bad: %#v", met) 487 } 488 if met.ConstraintFiltered["${attr.kernel.name} = freebsd"] != 1 { 489 t.Fatalf("bad: %#v", met) 490 } 491 } 492 493 func TestSystemStack_Select_BinPack_Overflow(t *testing.T) { 494 _, ctx := testContext(t) 495 nodes := []*structs.Node{ 496 mock.Node(), 497 mock.Node(), 498 } 499 zero := nodes[0] 500 zero.ReservedResources = &structs.NodeReservedResources{ 501 Cpu: structs.NodeReservedCpuResources{ 502 CpuShares: zero.NodeResources.Cpu.CpuShares, 503 }, 504 } 505 one := nodes[1] 506 507 stack := NewSystemStack(ctx) 508 stack.SetNodes(nodes) 509 510 job := mock.Job() 511 stack.SetJob(job) 512 513 selectOptions := &SelectOptions{} 514 node := stack.Select(job.TaskGroups[0], selectOptions) 515 ctx.Metrics().PopulateScoreMetaData() 516 if node == nil { 517 t.Fatalf("missing node %#v", ctx.Metrics()) 518 } 519 520 if node.Node != one { 521 t.Fatalf("bad") 522 } 523 524 met := ctx.Metrics() 525 if met.NodesExhausted != 1 { 526 t.Fatalf("bad: %#v", met) 527 } 528 if met.ClassExhausted["linux-medium-pci"] != 1 { 529 t.Fatalf("bad: %#v", met) 530 } 531 // Should have two scores, one from bin packing and one from normalization 532 if len(met.ScoreMetaData) != 1 { 533 t.Fatalf("bad: %#v", met) 534 } 535 }