Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Goroutine leak #165

Closed
3mard opened this issue Dec 25, 2023 · 5 comments · Fixed by #166, #167 or #168
Closed

Goroutine leak #165

3mard opened this issue Dec 25, 2023 · 5 comments · Fixed by #166, #167 or #168

Comments

@3mard
Copy link
Contributor

3mard commented Dec 25, 2023

I've noticed that running the speed test in a loop can cause a goroutine/memory leak

Consider the following code being run in a loop (sequentially )

	serverList, err := a.tst.FetchServers()
		if err != nil {
			fmt.Println(err)
			return
		}
		targets, _ := serverList.FindServer([]int{})
		if len(targets) == 0 {
			fmt.Println("No servers found")
			return
		}
		s := targets[0]
		s.PingTest(nil)
		s.DownloadTest()
		s.UploadTest()

		fmt.Printf("Latency: %s, Download: %f, Upload: %f\n", s.Latency, s.DLSpeed, s.ULSpeed)
		UploadSpeed.Set(s.ULSpeed)
		DownloadSpeed.Set(s.DLSpeed)
		Latency.Set(float64(s.Latency.Milliseconds()))
		s.Context.Reset() // reset counter
		a.tst.Reset()

Will result in an increasing goroutines count that seems to never decrease, eg:


1390 @ 0x4d398 0x5e5a8 0x2babfc 0x80894
#	0x2babfb	net/http.(*persistConn).readLoop+0xb7b	/usr/local/go/src/net/http/transport.go:2238

1390 @ 0x4d398 0x5e5a8 0x2bb99c 0x80894
#	0x2bb99b	net/http.(*persistConn).writeLoop+0x9b	/usr/local/go/src/net/http/transport.go:2421

Upon my investigation I found the following function that seems to me to be the source of the leak

func (s *Server) HTTPPing(
	ctx context.Context,
	echoTimes int,
	echoFreq time.Duration,
	callback func(latency time.Duration),
) (latencies []int64, err error) {
	u, err := url.Parse(s.URL)
	if err != nil || len(u.Host) == 0 {
		return nil, err
	}
	pingDst := fmt.Sprintf("%s/latency.txt", s.URL)
	dbg.Printf("Echo: %s\n", pingDst)
	failTimes := 0
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingDst, nil)
	if err != nil {
		return nil, err
	}
	for i := 0; i < echoTimes; i++ {
		sTime := time.Now()
		_, err = s.Context.doer.Do(req)
		endTime := time.Since(sTime)
		if err != nil {
			failTimes++
			continue
		}
		latencies = append(latencies, endTime.Nanoseconds()/2)
		dbg.Printf("2RTT: %s\n", endTime)
		if callback != nil {
			callback(endTime / 2)
		}
		time.Sleep(echoFreq)
	}
	if failTimes == echoTimes {
		return nil, ErrConnectTimeout
	}
	return
}

The problem lays in _, err = s.Context.doer.Do(req), According to the docs

If the returned error is nil, the Response will contain a non-nil Body which the user is expected to close. If the Body is not both read to EOF and closed, the Client's underlying RoundTripper (typically Transport) may not be able to re-use a persistent TCP connection to the server for a subsequent "keep-alive" request.

@r3inbowari
Copy link
Collaborator

@3mard, Thank you for your careful investigation!

@r3inbowari r3inbowari reopened this Dec 25, 2023
@r3inbowari
Copy link
Collaborator

But that change made func TestFetchServerList(t *testing.T) fail
https://github.com/showwin/speedtest-go/actions/runs/7324003922/job/19947154035

@3mard
Copy link
Contributor Author

3mard commented Dec 25, 2023

Sorry I made a mistake #167 should fix it

However TestFetchServerByID is still failing with
server_test.go:167: no server available or found

@r3inbowari
Copy link
Collaborator

Thank you, it works.

This was linked to pull requests Dec 25, 2023
@3mard
Copy link
Contributor Author

3mard commented Dec 26, 2023

Sure thing :) should we close this issue now ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants