Skip to content

Commit 10a88ca

Browse files
committed
add test and fix for goroutine leak when cancelling
1 parent 557db68 commit 10a88ca

3 files changed

Lines changed: 76 additions & 0 deletions

File tree

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ go:
99
- 1.6.x
1010
- 1.7.x
1111
- 1.8.x
12+
- 1.9.x
1213
- tip

fetch.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,11 @@ loop:
427427
}
428428
}
429429

430+
// need to drain ch until it is closed, to prevent the producer goroutine
431+
// from leaking.
432+
for _ = range ch {
433+
}
434+
430435
f.q.wg.Done()
431436
}
432437

fetch_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
package fetchbot
66

77
import (
8+
"fmt"
89
"io/ioutil"
910
"net/http"
1011
"net/http/httptest"
1112
"net/url"
13+
"runtime"
1214
"strconv"
1315
"sync"
1416
"testing"
@@ -716,3 +718,71 @@ func TestCancel(t *testing.T) {
716718
t.Errorf("expected no errors, got %d", cnt)
717719
}
718720
}
721+
722+
type doerFunc func(*http.Request) (*http.Response, error)
723+
724+
func (f doerFunc) Do(req *http.Request) (*http.Response, error) {
725+
return f(req)
726+
}
727+
728+
func TestGoroLeak(t *testing.T) {
729+
callCount := 0
730+
f := New(HandlerFunc(func(c *Context, res *http.Response, err error) {
731+
// sleep a bit so that it produces faster than it consumes
732+
callCount++
733+
time.Sleep(time.Millisecond)
734+
}))
735+
736+
f.HttpClient = doerFunc(func(req *http.Request) (*http.Response, error) {
737+
res := httptest.NewRecorder()
738+
return res.Result(), nil
739+
})
740+
741+
f.DisablePoliteness = true
742+
f.CrawlDelay = 0
743+
744+
startGoros := runtime.NumGoroutine()
745+
q := f.Start()
746+
747+
// start a goro that enqueues a new URL (always on the same domain)
748+
// until Send fails.
749+
wg := sync.WaitGroup{}
750+
wg.Add(1)
751+
counter := 0
752+
go func() {
753+
defer wg.Done()
754+
for {
755+
counter++
756+
_, err := q.SendStringGet(fmt.Sprintf("http://example.com/%d", counter))
757+
if err != nil {
758+
return
759+
}
760+
}
761+
}()
762+
763+
<-time.After(100 * time.Millisecond)
764+
q.Cancel()
765+
wg.Wait()
766+
767+
// if the race detector is set, may fail if num goroutine checked
768+
// immediately. But under normal circumstances, the goroutines
769+
// are released when Cancel/Close returns.
770+
time.Sleep(10 * time.Millisecond)
771+
772+
cancelGoros := runtime.NumGoroutine()
773+
774+
// should have sent a lot of URLs
775+
if counter < 10*callCount {
776+
t.Errorf("want many more Send than Calls, got %d and %d", counter, callCount)
777+
}
778+
// should have received between 10-100 calls
779+
if callCount < 10 || callCount > 100 {
780+
t.Errorf("want at least 10 and no more than 100 handler calls, got %d", callCount)
781+
}
782+
// should have the same number of goroutines as there was at the start
783+
if startGoros != cancelGoros {
784+
t.Errorf("want %d goros like there was at the start, got %d (leak)", startGoros, cancelGoros)
785+
}
786+
787+
t.Logf("start: %d, cancel: %d, counter: %d, calls: %d", startGoros, cancelGoros, counter, callCount)
788+
}

0 commit comments

Comments
 (0)