|
5 | 5 | package fetchbot |
6 | 6 |
|
7 | 7 | import ( |
| 8 | + "fmt" |
8 | 9 | "io/ioutil" |
9 | 10 | "net/http" |
10 | 11 | "net/http/httptest" |
11 | 12 | "net/url" |
| 13 | + "runtime" |
12 | 14 | "strconv" |
13 | 15 | "sync" |
14 | 16 | "testing" |
@@ -716,3 +718,71 @@ func TestCancel(t *testing.T) { |
716 | 718 | t.Errorf("expected no errors, got %d", cnt) |
717 | 719 | } |
718 | 720 | } |
| 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