Source file src/runtime/testdata/testgoroutineleakprofile/commonpatterns.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"os"
    11  	"runtime"
    12  	"runtime/pprof"
    13  	"time"
    14  )
    15  
    16  // Common goroutine leak patterns. Extracted from:
    17  // "Unveiling and Vanquishing Goroutine Leaks in Enterprise Microservices: A Dynamic Analysis Approach"
    18  // doi:10.1109/CGO57630.2024.10444835
    19  //
    20  // Tests in this file are not flaky iff. the test is run with GOMAXPROCS=1.
    21  // The main goroutine forcefully yields via `runtime.Gosched()` before
    22  // running the profiler. This moves them to the back of the run queue,
    23  // allowing the leaky goroutines to be scheduled beforehand and get stuck.
    24  
    25  func init() {
    26  	register("NoCloseRange", NoCloseRange)
    27  	register("MethodContractViolation", MethodContractViolation)
    28  	register("DoubleSend", DoubleSend)
    29  	register("EarlyReturn", EarlyReturn)
    30  	register("NCastLeak", NCastLeak)
    31  	register("Timeout", Timeout)
    32  }
    33  
    34  // Incoming list of items and the number of workers.
    35  func noCloseRange(list []any, workers int) {
    36  	ch := make(chan any)
    37  
    38  	// Create each worker
    39  	for i := 0; i < workers; i++ {
    40  		go func() {
    41  
    42  			// Each worker waits for an item and processes it.
    43  			for item := range ch {
    44  				// Process each item
    45  				_ = item
    46  			}
    47  		}()
    48  	}
    49  
    50  	// Send each item to one of the workers.
    51  	for _, item := range list {
    52  		// Sending can leak if workers == 0 or if one of the workers panics
    53  		ch <- item
    54  	}
    55  	// The channel is never closed, so workers leak once there are no more
    56  	// items left to process.
    57  }
    58  
    59  func NoCloseRange() {
    60  	prof := pprof.Lookup("goroutineleak")
    61  	defer func() {
    62  		time.Sleep(100 * time.Millisecond)
    63  		prof.WriteTo(os.Stdout, 2)
    64  	}()
    65  
    66  	go noCloseRange([]any{1, 2, 3}, 0)
    67  	go noCloseRange([]any{1, 2, 3}, 3)
    68  }
    69  
    70  // A worker processes items pushed to `ch` one by one in the background.
    71  // When the worker is no longer needed, it must be closed with `Stop`.
    72  //
    73  // Specifications:
    74  //
    75  //	A worker may be started any number of times, but must be stopped only once.
    76  //		Stopping a worker multiple times will lead to a close panic.
    77  //	Any worker that is started must eventually be stopped.
    78  //		Failing to stop a worker results in a goroutine leak
    79  type worker struct {
    80  	ch   chan any
    81  	done chan any
    82  }
    83  
    84  // Start spawns a background goroutine that extracts items pushed to the queue.
    85  func (w worker) Start() {
    86  	go func() {
    87  
    88  		for {
    89  			select {
    90  			case <-w.ch: // Normal workflow
    91  			case <-w.done:
    92  				return // Shut down
    93  			}
    94  		}
    95  	}()
    96  }
    97  
    98  func (w worker) Stop() {
    99  	// Allows goroutine created by Start to terminate
   100  	close(w.done)
   101  }
   102  
   103  func (w worker) AddToQueue(item any) {
   104  	w.ch <- item
   105  }
   106  
   107  // worker limited in scope by workerLifecycle
   108  func workerLifecycle(items []any) {
   109  	// Create a new worker
   110  	w := worker{
   111  		ch:   make(chan any),
   112  		done: make(chan any),
   113  	}
   114  	// Start worker
   115  	w.Start()
   116  
   117  	// Operate on worker
   118  	for _, item := range items {
   119  		w.AddToQueue(item)
   120  	}
   121  
   122  	runtime.Gosched()
   123  	// Exits without calling ’Stop’. Goroutine created by `Start` eventually leaks.
   124  }
   125  
   126  func MethodContractViolation() {
   127  	prof := pprof.Lookup("goroutineleak")
   128  	defer func() {
   129  		runtime.Gosched()
   130  		prof.WriteTo(os.Stdout, 2)
   131  	}()
   132  
   133  	workerLifecycle(make([]any, 10))
   134  	runtime.Gosched()
   135  }
   136  
   137  // doubleSend incoming channel must send a message (incoming error simulates an error generated internally).
   138  func doubleSend(ch chan any, err error) {
   139  	if err != nil {
   140  		// In case of an error, send nil.
   141  		ch <- nil
   142  		// Return is missing here.
   143  	}
   144  	// Otherwise, continue with normal behaviour
   145  	// This send is still executed in the error case, which may lead to a goroutine leak.
   146  	ch <- struct{}{}
   147  }
   148  
   149  func DoubleSend() {
   150  	prof := pprof.Lookup("goroutineleak")
   151  	ch := make(chan any)
   152  	defer func() {
   153  		runtime.Gosched()
   154  		prof.WriteTo(os.Stdout, 2)
   155  	}()
   156  
   157  	go func() {
   158  		doubleSend(ch, nil)
   159  	}()
   160  	<-ch
   161  
   162  	go func() {
   163  		doubleSend(ch, fmt.Errorf("error"))
   164  	}()
   165  	<-ch
   166  
   167  	ch1 := make(chan any, 1)
   168  	go func() {
   169  		doubleSend(ch1, fmt.Errorf("error"))
   170  	}()
   171  	<-ch1
   172  }
   173  
   174  // earlyReturn demonstrates a common pattern of goroutine leaks.
   175  // A return statement interrupts the evaluation of the parent goroutine before it can consume a message.
   176  // Incoming error simulates an error produced internally.
   177  func earlyReturn(err error) {
   178  	// Create a synchronous channel
   179  	ch := make(chan any)
   180  
   181  	go func() {
   182  
   183  		// Send something to the channel.
   184  		// Leaks if the parent goroutine terminates early.
   185  		ch <- struct{}{}
   186  	}()
   187  
   188  	if err != nil {
   189  		// Interrupt evaluation of parent early in case of error.
   190  		// Sender leaks.
   191  		return
   192  	}
   193  
   194  	// Only receive if there is no error.
   195  	<-ch
   196  }
   197  
   198  func EarlyReturn() {
   199  	prof := pprof.Lookup("goroutineleak")
   200  	defer func() {
   201  		runtime.Gosched()
   202  		prof.WriteTo(os.Stdout, 2)
   203  	}()
   204  
   205  	go earlyReturn(fmt.Errorf("error"))
   206  }
   207  
   208  // nCastLeak processes a number of items. First result to pass the post is retrieved from the channel queue.
   209  func nCastLeak(items []any) {
   210  	// Channel is synchronous.
   211  	ch := make(chan any)
   212  
   213  	// Iterate over every item
   214  	for range items {
   215  		go func() {
   216  
   217  			// Process item and send result to channel
   218  			ch <- struct{}{}
   219  			// Channel is synchronous: only one sender will synchronise
   220  		}()
   221  	}
   222  	// Retrieve first result. All other senders block.
   223  	// Receiver blocks if there are no senders.
   224  	<-ch
   225  }
   226  
   227  func NCastLeak() {
   228  	prof := pprof.Lookup("goroutineleak")
   229  	defer func() {
   230  		for i := 0; i < yieldCount; i++ {
   231  			// Yield enough times  to allow all the leaky goroutines to
   232  			// reach the execution point.
   233  			runtime.Gosched()
   234  		}
   235  		prof.WriteTo(os.Stdout, 2)
   236  	}()
   237  
   238  	go func() {
   239  		nCastLeak(nil)
   240  	}()
   241  
   242  	go func() {
   243  		nCastLeak(make([]any, 5))
   244  	}()
   245  }
   246  
   247  // A context is provided to short-circuit evaluation, leading
   248  // the sender goroutine to leak.
   249  func timeout(ctx context.Context) {
   250  	ch := make(chan any)
   251  
   252  	go func() {
   253  		ch <- struct{}{}
   254  	}()
   255  
   256  	select {
   257  	case <-ch: // Receive message
   258  		// Sender is released
   259  	case <-ctx.Done(): // Context was cancelled or timed out
   260  		// Sender is leaked
   261  	}
   262  }
   263  
   264  func Timeout() {
   265  	prof := pprof.Lookup("goroutineleak")
   266  	defer func() {
   267  		runtime.Gosched()
   268  		prof.WriteTo(os.Stdout, 2)
   269  	}()
   270  
   271  	ctx, cancel := context.WithCancel(context.Background())
   272  	cancel()
   273  
   274  	for i := 0; i < 100; i++ {
   275  		go timeout(ctx)
   276  	}
   277  }
   278  

View as plain text