Skip to content

Commit fa2e514

Browse files
bensynapsealitto
authored andcommitted
fix(pool): account for active workers when resizing
1 parent e309c37 commit fa2e514

2 files changed

Lines changed: 140 additions & 2 deletions

File tree

‎pool.go‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,11 @@ func (p *pool) Resize(maxConcurrency int) {
182182

183183
p.mutex.Lock()
184184

185-
// Calculate the number of new workers to launch to reach the new max concurrency or the number of tasks in the queue, whichever is smaller
186-
newWorkers := int(math.Min(float64(maxConcurrency-p.maxConcurrency), float64(p.tasks.Len())))
185+
// Workers from an earlier limit can still be running after a shrink.
186+
newWorkers := maxConcurrency - int(p.workerCount.Load())
187+
if queuedTasks := int(p.tasks.Len()); newWorkers > queuedTasks {
188+
newWorkers = queuedTasks
189+
}
187190

188191
p.maxConcurrency = maxConcurrency
189192

‎pool_test.go‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,141 @@ func TestPoolResize(t *testing.T) {
491491
pool.Stop().Wait()
492492
}
493493

494+
func TestPoolResizeBeforeWorkersRetire(t *testing.T) {
495+
for _, subpool := range []bool{false, true} {
496+
for _, test := range []struct {
497+
name string
498+
retired int
499+
maxWorkers int
500+
running int64
501+
startedQueued int
502+
}{
503+
{name: "still_above_limit", maxWorkers: 3, running: 5},
504+
{name: "partially_retired", retired: 2, maxWorkers: 4, running: 4, startedQueued: 1},
505+
{name: "grow_above_original_limit", maxWorkers: 7, running: 7, startedQueued: 2},
506+
} {
507+
name := test.name
508+
if subpool {
509+
name = "subpool/" + name
510+
}
511+
t.Run(name, func(t *testing.T) {
512+
var parent, pool Pool
513+
if subpool {
514+
parent = NewPool(10)
515+
pool = parent.NewSubpool(5)
516+
} else {
517+
pool = NewPool(5)
518+
}
519+
520+
started := make(chan struct{}, 5)
521+
queuedStarted := make(chan struct{}, 2)
522+
releaseQueued := make(chan struct{})
523+
releases := make([]chan struct{}, 5)
524+
for i := range releases {
525+
releases[i] = make(chan struct{})
526+
}
527+
released := 0
528+
t.Cleanup(func() {
529+
for _, release := range releases[released:] {
530+
close(release)
531+
}
532+
close(releaseQueued)
533+
pool.StopAndWait()
534+
if parent != nil {
535+
parent.StopAndWait()
536+
}
537+
})
538+
539+
for _, release := range releases {
540+
release := release
541+
pool.Submit(func() {
542+
started <- struct{}{}
543+
<-release
544+
})
545+
}
546+
for range releases {
547+
select {
548+
case <-started:
549+
case <-time.After(2 * time.Second):
550+
t.Fatal("timed out waiting for initial workers")
551+
}
552+
}
553+
for i := 0; i < 2; i++ {
554+
pool.Submit(func() {
555+
queuedStarted <- struct{}{}
556+
<-releaseQueued
557+
})
558+
}
559+
560+
pool.Resize(1)
561+
for _, release := range releases[:test.retired] {
562+
close(release)
563+
released++
564+
}
565+
deadline := time.Now().Add(2 * time.Second)
566+
for pool.RunningWorkers() != int64(5-test.retired) {
567+
if time.Now().After(deadline) {
568+
t.Fatal("timed out waiting for workers to retire")
569+
}
570+
time.Sleep(time.Millisecond)
571+
}
572+
573+
pool.Resize(test.maxWorkers)
574+
assert.Equal(t, test.maxWorkers, pool.MaxConcurrency())
575+
assert.Equal(t, test.running, pool.RunningWorkers())
576+
for i := 0; i < test.startedQueued; i++ {
577+
select {
578+
case <-queuedStarted:
579+
case <-time.After(2 * time.Second):
580+
t.Fatal("timed out waiting for queued tasks")
581+
}
582+
}
583+
assert.Equal(t, test.startedQueued, 2-int(pool.WaitingTasks()))
584+
if parent != nil {
585+
assert.Equal(t, uint64(5+2*test.startedQueued), parent.SubmittedTasks())
586+
}
587+
})
588+
}
589+
}
590+
}
591+
592+
func TestSubpoolResizeBeforeWorkersRetireWithFullParent(t *testing.T) {
593+
parent := NewPool(5, WithQueueSize(0))
594+
pool := parent.NewSubpool(5, WithQueueSize(2))
595+
started := make(chan struct{}, 5)
596+
release := make(chan struct{})
597+
resized := make(chan struct{})
598+
t.Cleanup(func() {
599+
close(release)
600+
<-resized
601+
pool.StopAndWait()
602+
parent.StopAndWait()
603+
})
604+
605+
for i := 0; i < 5; i++ {
606+
pool.Submit(func() {
607+
started <- struct{}{}
608+
<-release
609+
})
610+
}
611+
for i := 0; i < 5; i++ {
612+
<-started
613+
}
614+
pool.Submit(func() {})
615+
pool.Submit(func() {})
616+
pool.Resize(1)
617+
go func() {
618+
pool.Resize(3)
619+
close(resized)
620+
}()
621+
622+
select {
623+
case <-resized:
624+
case <-time.After(2 * time.Second):
625+
t.Fatal("resize blocked on the full parent while existing workers exceed the new limit")
626+
}
627+
}
628+
494629
func TestPoolResizeWithZeroMaxConcurrency(t *testing.T) {
495630
pool := NewPool(10)
496631

0 commit comments

Comments
 (0)