| // Copyright 2025 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "runtime" |
| "runtime/pprof" |
| "time" |
| ) |
| |
| // Common goroutine leak patterns. Extracted from: |
| // "Unveiling and Vanquishing Goroutine Leaks in Enterprise Microservices: A Dynamic Analysis Approach" |
| // doi:10.1109/CGO57630.2024.10444835 |
| // |
| // Tests in this file are not flaky iff. the test is run with GOMAXPROCS=1. |
| // The main goroutine forcefully yields via `runtime.Gosched()` before |
| // running the profiler. This moves them to the back of the run queue, |
| // allowing the leaky goroutines to be scheduled beforehand and get stuck. |
| |
| func init() { |
| register("NoCloseRange", NoCloseRange) |
| register("MethodContractViolation", MethodContractViolation) |
| register("DoubleSend", DoubleSend) |
| register("EarlyReturn", EarlyReturn) |
| register("NCastLeak", NCastLeak) |
| register("Timeout", Timeout) |
| } |
| |
| // Incoming list of items and the number of workers. |
| func noCloseRange(list []any, workers int) { |
| ch := make(chan any) |
| |
| // Create each worker |
| for i := 0; i < workers; i++ { |
| go func() { |
| |
| // Each worker waits for an item and processes it. |
| for item := range ch { |
| // Process each item |
| _ = item |
| } |
| }() |
| } |
| |
| // Send each item to one of the workers. |
| for _, item := range list { |
| // Sending can leak if workers == 0 or if one of the workers panics |
| ch <- item |
| } |
| // The channel is never closed, so workers leak once there are no more |
| // items left to process. |
| } |
| |
| func NoCloseRange() { |
| prof := pprof.Lookup("goroutineleak") |
| defer func() { |
| time.Sleep(100 * time.Millisecond) |
| prof.WriteTo(os.Stdout, 2) |
| }() |
| |
| go noCloseRange([]any{1, 2, 3}, 0) |
| go noCloseRange([]any{1, 2, 3}, 3) |
| } |
| |
| // A worker processes items pushed to `ch` one by one in the background. |
| // When the worker is no longer needed, it must be closed with `Stop`. |
| // |
| // Specifications: |
| // |
| // A worker may be started any number of times, but must be stopped only once. |
| // Stopping a worker multiple times will lead to a close panic. |
| // Any worker that is started must eventually be stopped. |
| // Failing to stop a worker results in a goroutine leak |
| type worker struct { |
| ch chan any |
| done chan any |
| } |
| |
| // Start spawns a background goroutine that extracts items pushed to the queue. |
| func (w worker) Start() { |
| go func() { |
| |
| for { |
| select { |
| case <-w.ch: // Normal workflow |
| case <-w.done: |
| return // Shut down |
| } |
| } |
| }() |
| } |
| |
| func (w worker) Stop() { |
| // Allows goroutine created by Start to terminate |
| close(w.done) |
| } |
| |
| func (w worker) AddToQueue(item any) { |
| w.ch <- item |
| } |
| |
| // worker limited in scope by workerLifecycle |
| func workerLifecycle(items []any) { |
| // Create a new worker |
| w := worker{ |
| ch: make(chan any), |
| done: make(chan any), |
| } |
| // Start worker |
| w.Start() |
| |
| // Operate on worker |
| for _, item := range items { |
| w.AddToQueue(item) |
| } |
| |
| runtime.Gosched() |
| // Exits without calling ’Stop’. Goroutine created by `Start` eventually leaks. |
| } |
| |
| func MethodContractViolation() { |
| prof := pprof.Lookup("goroutineleak") |
| defer func() { |
| runtime.Gosched() |
| prof.WriteTo(os.Stdout, 2) |
| }() |
| |
| workerLifecycle(make([]any, 10)) |
| runtime.Gosched() |
| } |
| |
| // doubleSend incoming channel must send a message (incoming error simulates an error generated internally). |
| func doubleSend(ch chan any, err error) { |
| if err != nil { |
| // In case of an error, send nil. |
| ch <- nil |
| // Return is missing here. |
| } |
| // Otherwise, continue with normal behaviour |
| // This send is still executed in the error case, which may lead to a goroutine leak. |
| ch <- struct{}{} |
| } |
| |
| func DoubleSend() { |
| prof := pprof.Lookup("goroutineleak") |
| ch := make(chan any) |
| defer func() { |
| runtime.Gosched() |
| prof.WriteTo(os.Stdout, 2) |
| }() |
| |
| go func() { |
| doubleSend(ch, nil) |
| }() |
| <-ch |
| |
| go func() { |
| doubleSend(ch, fmt.Errorf("error")) |
| }() |
| <-ch |
| |
| ch1 := make(chan any, 1) |
| go func() { |
| doubleSend(ch1, fmt.Errorf("error")) |
| }() |
| <-ch1 |
| } |
| |
| // earlyReturn demonstrates a common pattern of goroutine leaks. |
| // A return statement interrupts the evaluation of the parent goroutine before it can consume a message. |
| // Incoming error simulates an error produced internally. |
| func earlyReturn(err error) { |
| // Create a synchronous channel |
| ch := make(chan any) |
| |
| go func() { |
| |
| // Send something to the channel. |
| // Leaks if the parent goroutine terminates early. |
| ch <- struct{}{} |
| }() |
| |
| if err != nil { |
| // Interrupt evaluation of parent early in case of error. |
| // Sender leaks. |
| return |
| } |
| |
| // Only receive if there is no error. |
| <-ch |
| } |
| |
| func EarlyReturn() { |
| prof := pprof.Lookup("goroutineleak") |
| defer func() { |
| runtime.Gosched() |
| prof.WriteTo(os.Stdout, 2) |
| }() |
| |
| go earlyReturn(fmt.Errorf("error")) |
| } |
| |
| // nCastLeak processes a number of items. First result to pass the post is retrieved from the channel queue. |
| func nCastLeak(items []any) { |
| // Channel is synchronous. |
| ch := make(chan any) |
| |
| // Iterate over every item |
| for range items { |
| go func() { |
| |
| // Process item and send result to channel |
| ch <- struct{}{} |
| // Channel is synchronous: only one sender will synchronise |
| }() |
| } |
| // Retrieve first result. All other senders block. |
| // Receiver blocks if there are no senders. |
| <-ch |
| } |
| |
| func NCastLeak() { |
| prof := pprof.Lookup("goroutineleak") |
| defer func() { |
| for i := 0; i < yieldCount; i++ { |
| // Yield enough times to allow all the leaky goroutines to |
| // reach the execution point. |
| runtime.Gosched() |
| } |
| prof.WriteTo(os.Stdout, 2) |
| }() |
| |
| go func() { |
| nCastLeak(nil) |
| }() |
| |
| go func() { |
| nCastLeak(make([]any, 5)) |
| }() |
| } |
| |
| // A context is provided to short-circuit evaluation, leading |
| // the sender goroutine to leak. |
| func timeout(ctx context.Context) { |
| ch := make(chan any) |
| |
| go func() { |
| ch <- struct{}{} |
| }() |
| |
| select { |
| case <-ch: // Receive message |
| // Sender is released |
| case <-ctx.Done(): // Context was cancelled or timed out |
| // Sender is leaked |
| } |
| } |
| |
| func Timeout() { |
| prof := pprof.Lookup("goroutineleak") |
| defer func() { |
| runtime.Gosched() |
| prof.WriteTo(os.Stdout, 2) |
| }() |
| |
| ctx, cancel := context.WithCancel(context.Background()) |
| cancel() |
| |
| for i := 0; i < 100; i++ { |
| go timeout(ctx) |
| } |
| } |