blob: 353e48ee7034f7938caef2f7e50b79c82f2bfc6e [file] [log] [blame]
// 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)
}
}