github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/client/gc_test.go (about) 1 package client 2 3 import ( 4 "fmt" 5 "testing" 6 "time" 7 8 "github.com/hashicorp/nomad/client/allocrunner" 9 "github.com/hashicorp/nomad/client/config" 10 "github.com/hashicorp/nomad/client/stats" 11 "github.com/hashicorp/nomad/helper/testlog" 12 "github.com/hashicorp/nomad/nomad" 13 "github.com/hashicorp/nomad/nomad/mock" 14 "github.com/hashicorp/nomad/nomad/structs" 15 "github.com/hashicorp/nomad/testutil" 16 "github.com/stretchr/testify/require" 17 ) 18 19 func gcConfig() *GCConfig { 20 return &GCConfig{ 21 DiskUsageThreshold: 80, 22 InodeUsageThreshold: 70, 23 Interval: 1 * time.Minute, 24 ReservedDiskMB: 0, 25 MaxAllocs: 100, 26 } 27 } 28 29 // exitAllocRunner is a helper that updates the allocs on the given alloc 30 // runners to be terminal 31 func exitAllocRunner(runners ...AllocRunner) { 32 for _, ar := range runners { 33 terminalAlloc := ar.Alloc().Copy() 34 terminalAlloc.DesiredStatus = structs.AllocDesiredStatusStop 35 ar.Update(terminalAlloc) 36 } 37 } 38 39 func TestIndexedGCAllocPQ(t *testing.T) { 40 t.Parallel() 41 pq := NewIndexedGCAllocPQ() 42 43 ar1, cleanup1 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 44 defer cleanup1() 45 ar2, cleanup2 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 46 defer cleanup2() 47 ar3, cleanup3 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 48 defer cleanup3() 49 ar4, cleanup4 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 50 defer cleanup4() 51 52 pq.Push(ar1.Alloc().ID, ar1) 53 pq.Push(ar2.Alloc().ID, ar2) 54 pq.Push(ar3.Alloc().ID, ar3) 55 pq.Push(ar4.Alloc().ID, ar4) 56 57 allocID := pq.Pop().allocRunner.Alloc().ID 58 if allocID != ar1.Alloc().ID { 59 t.Fatalf("expected alloc %v, got %v", allocID, ar1.Alloc().ID) 60 } 61 62 allocID = pq.Pop().allocRunner.Alloc().ID 63 if allocID != ar2.Alloc().ID { 64 t.Fatalf("expected alloc %v, got %v", allocID, ar1.Alloc().ID) 65 } 66 67 allocID = pq.Pop().allocRunner.Alloc().ID 68 if allocID != ar3.Alloc().ID { 69 t.Fatalf("expected alloc %v, got %v", allocID, ar1.Alloc().ID) 70 } 71 72 allocID = pq.Pop().allocRunner.Alloc().ID 73 if allocID != ar4.Alloc().ID { 74 t.Fatalf("expected alloc %v, got %v", allocID, ar1.Alloc().ID) 75 } 76 77 gcAlloc := pq.Pop() 78 if gcAlloc != nil { 79 t.Fatalf("expected nil, got %v", gcAlloc) 80 } 81 } 82 83 // MockAllocCounter implements AllocCounter interface. 84 type MockAllocCounter struct { 85 allocs int 86 } 87 88 func (m *MockAllocCounter) NumAllocs() int { 89 return m.allocs 90 } 91 92 type MockStatsCollector struct { 93 availableValues []uint64 94 usedPercents []float64 95 inodePercents []float64 96 index int 97 } 98 99 func (m *MockStatsCollector) Collect() error { 100 return nil 101 } 102 103 func (m *MockStatsCollector) Stats() *stats.HostStats { 104 if len(m.availableValues) == 0 { 105 return nil 106 } 107 108 available := m.availableValues[m.index] 109 usedPercent := m.usedPercents[m.index] 110 inodePercent := m.inodePercents[m.index] 111 112 if m.index < len(m.availableValues)-1 { 113 m.index = m.index + 1 114 } 115 return &stats.HostStats{ 116 AllocDirStats: &stats.DiskStats{ 117 Available: available, 118 UsedPercent: usedPercent, 119 InodesUsedPercent: inodePercent, 120 }, 121 } 122 } 123 124 func TestAllocGarbageCollector_MarkForCollection(t *testing.T) { 125 t.Parallel() 126 logger := testlog.HCLogger(t) 127 gc := NewAllocGarbageCollector(logger, &MockStatsCollector{}, &MockAllocCounter{}, gcConfig()) 128 129 ar1, cleanup1 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 130 defer cleanup1() 131 132 gc.MarkForCollection(ar1.Alloc().ID, ar1) 133 134 gcAlloc := gc.allocRunners.Pop() 135 if gcAlloc == nil || gcAlloc.allocRunner != ar1 { 136 t.Fatalf("bad gcAlloc: %v", gcAlloc) 137 } 138 } 139 140 func TestAllocGarbageCollector_Collect(t *testing.T) { 141 t.Parallel() 142 logger := testlog.HCLogger(t) 143 gc := NewAllocGarbageCollector(logger, &MockStatsCollector{}, &MockAllocCounter{}, gcConfig()) 144 145 ar1, cleanup1 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 146 defer cleanup1() 147 ar2, cleanup2 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 148 defer cleanup2() 149 150 go ar1.Run() 151 go ar2.Run() 152 153 gc.MarkForCollection(ar1.Alloc().ID, ar1) 154 gc.MarkForCollection(ar2.Alloc().ID, ar2) 155 156 // Exit the alloc runners 157 exitAllocRunner(ar1, ar2) 158 159 gc.Collect(ar1.Alloc().ID) 160 gcAlloc := gc.allocRunners.Pop() 161 if gcAlloc == nil || gcAlloc.allocRunner != ar2 { 162 t.Fatalf("bad gcAlloc: %v", gcAlloc) 163 } 164 } 165 166 func TestAllocGarbageCollector_CollectAll(t *testing.T) { 167 t.Parallel() 168 logger := testlog.HCLogger(t) 169 gc := NewAllocGarbageCollector(logger, &MockStatsCollector{}, &MockAllocCounter{}, gcConfig()) 170 171 ar1, cleanup1 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 172 defer cleanup1() 173 ar2, cleanup2 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 174 defer cleanup2() 175 176 gc.MarkForCollection(ar1.Alloc().ID, ar1) 177 gc.MarkForCollection(ar2.Alloc().ID, ar2) 178 179 gc.CollectAll() 180 gcAlloc := gc.allocRunners.Pop() 181 if gcAlloc != nil { 182 t.Fatalf("bad gcAlloc: %v", gcAlloc) 183 } 184 } 185 186 func TestAllocGarbageCollector_MakeRoomForAllocations_EnoughSpace(t *testing.T) { 187 t.Parallel() 188 logger := testlog.HCLogger(t) 189 statsCollector := &MockStatsCollector{} 190 conf := gcConfig() 191 conf.ReservedDiskMB = 20 192 gc := NewAllocGarbageCollector(logger, statsCollector, &MockAllocCounter{}, conf) 193 194 ar1, cleanup1 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 195 defer cleanup1() 196 ar2, cleanup2 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 197 defer cleanup2() 198 199 go ar1.Run() 200 go ar2.Run() 201 202 gc.MarkForCollection(ar1.Alloc().ID, ar1) 203 gc.MarkForCollection(ar2.Alloc().ID, ar2) 204 205 // Exit the alloc runners 206 exitAllocRunner(ar1, ar2) 207 208 // Make stats collector report 200MB free out of which 20MB is reserved 209 statsCollector.availableValues = []uint64{200 * MB} 210 statsCollector.usedPercents = []float64{0} 211 statsCollector.inodePercents = []float64{0} 212 213 alloc := mock.Alloc() 214 alloc.AllocatedResources.Shared.DiskMB = 150 215 if err := gc.MakeRoomFor([]*structs.Allocation{alloc}); err != nil { 216 t.Fatalf("err: %v", err) 217 } 218 219 // When we have enough disk available and don't need to do any GC so we 220 // should have two ARs in the GC queue 221 for i := 0; i < 2; i++ { 222 if gcAlloc := gc.allocRunners.Pop(); gcAlloc == nil { 223 t.Fatalf("err: %v", gcAlloc) 224 } 225 } 226 } 227 228 func TestAllocGarbageCollector_MakeRoomForAllocations_GC_Partial(t *testing.T) { 229 t.Parallel() 230 logger := testlog.HCLogger(t) 231 statsCollector := &MockStatsCollector{} 232 conf := gcConfig() 233 conf.ReservedDiskMB = 20 234 gc := NewAllocGarbageCollector(logger, statsCollector, &MockAllocCounter{}, conf) 235 236 ar1, cleanup1 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 237 defer cleanup1() 238 ar2, cleanup2 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 239 defer cleanup2() 240 241 go ar1.Run() 242 go ar2.Run() 243 244 gc.MarkForCollection(ar1.Alloc().ID, ar1) 245 gc.MarkForCollection(ar2.Alloc().ID, ar2) 246 247 // Exit the alloc runners 248 exitAllocRunner(ar1, ar2) 249 250 // Make stats collector report 80MB and 175MB free in subsequent calls 251 statsCollector.availableValues = []uint64{80 * MB, 80 * MB, 175 * MB} 252 statsCollector.usedPercents = []float64{0, 0, 0} 253 statsCollector.inodePercents = []float64{0, 0, 0} 254 255 alloc := mock.Alloc() 256 alloc.AllocatedResources.Shared.DiskMB = 150 257 if err := gc.MakeRoomFor([]*structs.Allocation{alloc}); err != nil { 258 t.Fatalf("err: %v", err) 259 } 260 261 // We should be GC-ing one alloc 262 if gcAlloc := gc.allocRunners.Pop(); gcAlloc == nil { 263 t.Fatalf("err: %v", gcAlloc) 264 } 265 266 if gcAlloc := gc.allocRunners.Pop(); gcAlloc != nil { 267 t.Fatalf("gcAlloc: %v", gcAlloc) 268 } 269 } 270 271 func TestAllocGarbageCollector_MakeRoomForAllocations_GC_All(t *testing.T) { 272 t.Parallel() 273 logger := testlog.HCLogger(t) 274 statsCollector := &MockStatsCollector{} 275 conf := gcConfig() 276 conf.ReservedDiskMB = 20 277 gc := NewAllocGarbageCollector(logger, statsCollector, &MockAllocCounter{}, conf) 278 279 ar1, cleanup1 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 280 defer cleanup1() 281 ar2, cleanup2 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 282 defer cleanup2() 283 284 go ar1.Run() 285 go ar2.Run() 286 287 gc.MarkForCollection(ar1.Alloc().ID, ar1) 288 gc.MarkForCollection(ar2.Alloc().ID, ar2) 289 290 // Exit the alloc runners 291 exitAllocRunner(ar1, ar2) 292 293 // Make stats collector report 80MB and 95MB free in subsequent calls 294 statsCollector.availableValues = []uint64{80 * MB, 80 * MB, 95 * MB} 295 statsCollector.usedPercents = []float64{0, 0, 0} 296 statsCollector.inodePercents = []float64{0, 0, 0} 297 298 alloc := mock.Alloc() 299 alloc.AllocatedResources.Shared.DiskMB = 150 300 if err := gc.MakeRoomFor([]*structs.Allocation{alloc}); err != nil { 301 t.Fatalf("err: %v", err) 302 } 303 304 // We should be GC-ing all the alloc runners 305 if gcAlloc := gc.allocRunners.Pop(); gcAlloc != nil { 306 t.Fatalf("gcAlloc: %v", gcAlloc) 307 } 308 } 309 310 func TestAllocGarbageCollector_MakeRoomForAllocations_GC_Fallback(t *testing.T) { 311 t.Parallel() 312 logger := testlog.HCLogger(t) 313 statsCollector := &MockStatsCollector{} 314 conf := gcConfig() 315 conf.ReservedDiskMB = 20 316 gc := NewAllocGarbageCollector(logger, statsCollector, &MockAllocCounter{}, conf) 317 318 ar1, cleanup1 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 319 cleanup1() 320 ar2, cleanup2 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 321 cleanup2() 322 323 go ar1.Run() 324 go ar2.Run() 325 326 gc.MarkForCollection(ar1.Alloc().ID, ar1) 327 gc.MarkForCollection(ar2.Alloc().ID, ar2) 328 329 // Exit the alloc runners 330 exitAllocRunner(ar1, ar2) 331 332 alloc := mock.Alloc() 333 alloc.AllocatedResources.Shared.DiskMB = 150 334 if err := gc.MakeRoomFor([]*structs.Allocation{alloc}); err != nil { 335 t.Fatalf("err: %v", err) 336 } 337 338 // We should be GC-ing one alloc 339 if gcAlloc := gc.allocRunners.Pop(); gcAlloc == nil { 340 t.Fatalf("err: %v", gcAlloc) 341 } 342 343 if gcAlloc := gc.allocRunners.Pop(); gcAlloc != nil { 344 t.Fatalf("gcAlloc: %v", gcAlloc) 345 } 346 } 347 348 // TestAllocGarbageCollector_MakeRoomFor_MaxAllocs asserts that when making room for new 349 // allocs, terminal allocs are GC'd until old_allocs + new_allocs <= limit 350 func TestAllocGarbageCollector_MakeRoomFor_MaxAllocs(t *testing.T) { 351 const maxAllocs = 6 352 require := require.New(t) 353 354 server, serverAddr, cleanupS := testServer(t, nil) 355 defer cleanupS() 356 testutil.WaitForLeader(t, server.RPC) 357 358 client, cleanup := TestClient(t, func(c *config.Config) { 359 c.GCMaxAllocs = maxAllocs 360 c.GCDiskUsageThreshold = 100 361 c.GCInodeUsageThreshold = 100 362 c.GCParallelDestroys = 1 363 c.GCInterval = time.Hour 364 c.RPCHandler = server 365 c.Servers = []string{serverAddr} 366 c.ConsulConfig.ClientAutoJoin = new(bool) 367 }) 368 defer cleanup() 369 waitTilNodeReady(client, t) 370 371 job := mock.Job() 372 job.TaskGroups[0].Count = 1 373 job.TaskGroups[0].Tasks[0].Driver = "mock_driver" 374 job.TaskGroups[0].Tasks[0].Config = map[string]interface{}{ 375 "run_for": "30s", 376 } 377 378 index := uint64(98) 379 nextIndex := func() uint64 { 380 index++ 381 return index 382 } 383 384 upsertJobFn := func(server *nomad.Server, j *structs.Job) { 385 state := server.State() 386 require.NoError(state.UpsertJob(structs.MsgTypeTestSetup, nextIndex(), j)) 387 require.NoError(state.UpsertJobSummary(nextIndex(), mock.JobSummary(j.ID))) 388 } 389 390 // Insert the Job 391 upsertJobFn(server, job) 392 393 upsertAllocFn := func(server *nomad.Server, a *structs.Allocation) { 394 state := server.State() 395 require.NoError(state.UpsertAllocs(structs.MsgTypeTestSetup, nextIndex(), []*structs.Allocation{a})) 396 } 397 398 upsertNewAllocFn := func(server *nomad.Server, j *structs.Job) *structs.Allocation { 399 alloc := mock.Alloc() 400 alloc.Job = j 401 alloc.JobID = j.ID 402 alloc.NodeID = client.NodeID() 403 404 upsertAllocFn(server, alloc) 405 406 return alloc.Copy() 407 } 408 409 var allocations []*structs.Allocation 410 411 // Fill the node with allocations 412 for i := 0; i < maxAllocs; i++ { 413 allocations = append(allocations, upsertNewAllocFn(server, job)) 414 } 415 416 // Wait until the allocations are ready 417 testutil.WaitForResult(func() (bool, error) { 418 ar := len(client.getAllocRunners()) 419 420 return ar == maxAllocs, fmt.Errorf("Expected %d allocs, got %d", maxAllocs, ar) 421 }, func(err error) { 422 t.Fatalf("Allocs did not start: %v", err) 423 }) 424 425 // Mark the first three as terminal 426 for i := 0; i < 3; i++ { 427 allocations[i].DesiredStatus = structs.AllocDesiredStatusStop 428 upsertAllocFn(server, allocations[i].Copy()) 429 } 430 431 // Wait until the allocations are stopped 432 testutil.WaitForResult(func() (bool, error) { 433 ar := client.getAllocRunners() 434 stopped := 0 435 for _, r := range ar { 436 if r.Alloc().TerminalStatus() { 437 stopped++ 438 } 439 } 440 441 return stopped == 3, fmt.Errorf("Expected %d terminal allocs, got %d", 3, stopped) 442 }, func(err error) { 443 t.Fatalf("Allocs did not terminate: %v", err) 444 }) 445 446 // Upsert a new allocation 447 // This does not get appended to `allocations` as we do not use them again. 448 upsertNewAllocFn(server, job) 449 450 // A single allocation should be GC'd 451 testutil.WaitForResult(func() (bool, error) { 452 ar := client.getAllocRunners() 453 destroyed := 0 454 for _, r := range ar { 455 if r.IsDestroyed() { 456 destroyed++ 457 } 458 } 459 460 return destroyed == 1, fmt.Errorf("Expected %d gc'd ars, got %d", 1, destroyed) 461 }, func(err error) { 462 t.Fatalf("Allocs did not get GC'd: %v", err) 463 }) 464 465 // Upsert a new allocation 466 // This does not get appended to `allocations` as we do not use them again. 467 upsertNewAllocFn(server, job) 468 469 // 2 allocations should be GC'd 470 testutil.WaitForResult(func() (bool, error) { 471 ar := client.getAllocRunners() 472 destroyed := 0 473 for _, r := range ar { 474 if r.IsDestroyed() { 475 destroyed++ 476 } 477 } 478 479 return destroyed == 2, fmt.Errorf("Expected %d gc'd ars, got %d", 2, destroyed) 480 }, func(err error) { 481 t.Fatalf("Allocs did not get GC'd: %v", err) 482 }) 483 484 // check that all 8 get run eventually 485 testutil.WaitForResult(func() (bool, error) { 486 ar := client.getAllocRunners() 487 if len(ar) != 8 { 488 return false, fmt.Errorf("expected 8 ARs, found %d: %v", len(ar), ar) 489 } 490 return true, nil 491 }, func(err error) { 492 require.NoError(err) 493 }) 494 } 495 496 func TestAllocGarbageCollector_UsageBelowThreshold(t *testing.T) { 497 t.Parallel() 498 logger := testlog.HCLogger(t) 499 statsCollector := &MockStatsCollector{} 500 conf := gcConfig() 501 conf.ReservedDiskMB = 20 502 gc := NewAllocGarbageCollector(logger, statsCollector, &MockAllocCounter{}, conf) 503 504 ar1, cleanup1 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 505 defer cleanup1() 506 ar2, cleanup2 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 507 defer cleanup2() 508 509 go ar1.Run() 510 go ar2.Run() 511 512 gc.MarkForCollection(ar1.Alloc().ID, ar1) 513 gc.MarkForCollection(ar2.Alloc().ID, ar2) 514 515 // Exit the alloc runners 516 exitAllocRunner(ar1, ar2) 517 518 statsCollector.availableValues = []uint64{1000} 519 statsCollector.usedPercents = []float64{20} 520 statsCollector.inodePercents = []float64{10} 521 522 if err := gc.keepUsageBelowThreshold(); err != nil { 523 t.Fatalf("err: %v", err) 524 } 525 526 // We shouldn't GC any of the allocs since the used percent values are below 527 // threshold 528 for i := 0; i < 2; i++ { 529 if gcAlloc := gc.allocRunners.Pop(); gcAlloc == nil { 530 t.Fatalf("err: %v", gcAlloc) 531 } 532 } 533 } 534 535 func TestAllocGarbageCollector_UsedPercentThreshold(t *testing.T) { 536 t.Parallel() 537 logger := testlog.HCLogger(t) 538 statsCollector := &MockStatsCollector{} 539 conf := gcConfig() 540 conf.ReservedDiskMB = 20 541 gc := NewAllocGarbageCollector(logger, statsCollector, &MockAllocCounter{}, conf) 542 543 ar1, cleanup1 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 544 defer cleanup1() 545 ar2, cleanup2 := allocrunner.TestAllocRunnerFromAlloc(t, mock.Alloc()) 546 defer cleanup2() 547 548 go ar1.Run() 549 go ar2.Run() 550 551 gc.MarkForCollection(ar1.Alloc().ID, ar1) 552 gc.MarkForCollection(ar2.Alloc().ID, ar2) 553 554 // Exit the alloc runners 555 exitAllocRunner(ar1, ar2) 556 557 statsCollector.availableValues = []uint64{1000, 800} 558 statsCollector.usedPercents = []float64{85, 60} 559 statsCollector.inodePercents = []float64{50, 30} 560 561 if err := gc.keepUsageBelowThreshold(); err != nil { 562 t.Fatalf("err: %v", err) 563 } 564 565 // We should be GC-ing only one of the alloc runners since the second time 566 // used percent returns a number below threshold. 567 if gcAlloc := gc.allocRunners.Pop(); gcAlloc == nil { 568 t.Fatalf("err: %v", gcAlloc) 569 } 570 571 if gcAlloc := gc.allocRunners.Pop(); gcAlloc != nil { 572 t.Fatalf("gcAlloc: %v", gcAlloc) 573 } 574 }