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

Easy to leak goroutines by not reading channel returned by Iter #6

Open
george-angel opened this issue Sep 8, 2024 · 0 comments
Open

Comments

@george-angel
Copy link

george-angel commented Sep 8, 2024

We run https://github.com/cesanta/docker_auth/ (which uses this package) and noticed a memory leak. If you check the heap - most of the memory is used by runtime.malg. If you then check goroutines, iterate holds most of them:

$ go tool pprof 2024-09-07T16:13:49+1000_goroutine.out
File: auth_server
Build ID: f0023e9b8e89438ff941b55f3266cd69dfff1f88
Type: goroutine
Time: Sep 7, 2024 at 4:13pm (AEST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top5
Showing nodes accounting for 10539, 100% of 10542 total
Dropped 45 nodes (cum <= 52)
Showing top 5 nodes out of 9
      flat  flat%   sum%        cum   cum%
     10539   100%   100%      10539   100%  runtime.gopark
         0     0%   100%       4889 46.38%  github.com/schwarmco/go-cartesian-product.Iter.func1
         0     0%   100%       5638 53.48%  github.com/schwarmco/go-cartesian-product.iterate
         0     0%   100%       5638 53.48%  runtime.chansend
         0     0%   100%       5638 53.48%  runtime.chansend1
(pprof)

Thats because it calls Iter and then breaks on a match: https://github.com/cesanta/docker_auth/blob/main/auth_server/authz/acl.go#L183-L192 .

iterate will block on trying to append: https://github.com/schwarmco/go-cartesian-product/blob/master/cartesian.go#L21 - but nothing will ever read from that channel.

I created a new func that takes a context arg, and changed Iter to use default context:

diff --git a/cartesian.go b/cartesian.go
index 2a64b73..80e2965 100644
--- a/cartesian.go
+++ b/cartesian.go
@@ -1,28 +1,47 @@
 package cartesian
 
+import "context"
+
 // Iter takes interface-slices and returns a channel, receiving cartesian products
 func Iter(params ...[]interface{}) chan []interface{} {
-	// create channel
+	return IterWithContext(context.Background(), params...)
+}
+
+func IterWithContext(ctx context.Context, params ...[]interface{}) <-chan []interface{} {
 	c := make(chan []interface{})
+
 	if len(params) == 0 {
 		close(c)
-		return c // Return a safe value for nil/empty params.
+		return c
 	}
+
 	go func() {
-		iterate(c, params[0], []interface{}{}, params[1:]...)
-		close(c)
+		defer close(c) // Ensure the channel is closed when the goroutine exits
+
+		iterate(ctx, c, params[0], []interface{}{}, params[1:]...)
 	}()
+
 	return c
 }
 
-func iterate(channel chan []interface{}, topLevel, result []interface{}, needUnpacking ...[]interface{}) {
+func iterate(ctx context.Context, channel chan []interface{}, topLevel, result []interface{}, needUnpacking ...[]interface{}) {
 	if len(needUnpacking) == 0 {
 		for _, p := range topLevel {
-			channel <- append(append([]interface{}{}, result...), p)
+			select {
+			case <-ctx.Done():
+				return // Exit if the context is canceled
+			case channel <- append(append([]interface{}{}, result...), p):
+			}
 		}
 		return
 	}
+
 	for _, p := range topLevel {
-		iterate(channel, needUnpacking[0], append(result, p), needUnpacking[1:]...)
+		select {
+		case <-ctx.Done():
+			return // Exit if the context is canceled
+		default:
+			iterate(ctx, channel, needUnpacking[0], append(result, p), needUnpacking[1:]...)
+		}
 	}
 }

I suspect this might be a common problem where consumers don't wait for all the data to be written to the channel and the channel to close before moving on, and are leaking goroutines.

Let me know what you think and if you want me to raise a PR, thanks!

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

No branches or pull requests

1 participant