Repository: kubernetes-sigs/azurefile-csi-driver
Pull request #2520 (2025-04-17 02:43:49)

[release-1.30] fix: goroutine leak when timeout
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/release.md#issue-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. Follow the instructions for writing a release note: https://git.k8s.io/community/contributors/guide/release-notes.md
6. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

**What type of PR is this?**
/kind bug

**What this PR does / why we need it**:
fix: goroutine leak when timeout

cherrypick of https://github.com/kubernetes-sigs/azurefile-csi-driver/pull/2516

**Which issue(s) this PR fixes**:
<!-- 
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

**Requirements**:
- [ ] uses [conventional commit messages](https://www.conventionalcommits.org/)
  <!-- Common commit types:
        build: Build 🏭
        chore: Maintenance 🔧
        ci: Continuous Integration 💜
        docs: Documentation 📘
        feat: Features 🌈
        fix: Bug Fixes 🐞
        perf: Performance Improvements 🚀
        refactor: Code Refactoring 💎
        revert: Revert Change ◀️
        style: Code Style 🎶
        security: Security Fix 🛡️
        test: Testing 💚 -->
- [ ] includes documentation
- [ ] adds unit tests
- [ ] tested upgrade from previous version

**Special notes for your reviewer**:


**Release note**:
```
fix: goroutine leak when timeout
```



Patch:
diff --git a/go.mod b/go.mod
index 65fefcd30..87bd11deb 100644
--- a/go.mod
+++ b/go.mod
@@ -25,6 +25,7 @@ require (
 	github.com/pborman/uuid v1.2.0
 	github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021
 	github.com/stretchr/testify v1.9.0
+	go.uber.org/goleak v1.3.0
 	go.uber.org/mock v0.4.0
 	golang.org/x/net v0.37.0
 	golang.org/x/sys v0.31.0
diff --git a/pkg/util/util.go b/pkg/util/util.go
index 86abd83a1..de625c64c 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -217,7 +217,7 @@ type TimeoutFunc func() (err error)
 // WaitUntilTimeout waits for the exec function to complete or return timeout error
 func WaitUntilTimeout(timeout time.Duration, execFunc ExecFunc, timeoutFunc TimeoutFunc) error {
 	// Create a channel to receive the result of the azcopy exec function
-	done := make(chan bool)
+	done := make(chan bool, 1)
 	var err error
 
 	// Start exec function in a goroutine
diff --git a/vendor/go.uber.org/goleak/.gitignore b/vendor/go.uber.org/goleak/.gitignore
new file mode 100644
index 000000000..0fff519a4
--- /dev/null
+++ b/vendor/go.uber.org/goleak/.gitignore
@@ -0,0 +1,5 @@
+vendor/
+/bin
+/lint.log
+/cover.out
+/cover.html
diff --git a/vendor/go.uber.org/goleak/.golangci.yml b/vendor/go.uber.org/goleak/.golangci.yml
new file mode 100644
index 000000000..f84e6da8b
--- /dev/null
+++ b/vendor/go.uber.org/goleak/.golangci.yml
@@ -0,0 +1,28 @@
+output:
+  # Make output more digestible with quickfix in vim/emacs/etc.
+  sort-results: true
+  print-issued-lines: false
+
+linters:
+  enable:
+    - gofumpt
+    - nolintlint
+    - revive
+
+linters-settings:
+  govet:
+    # These govet checks are disabled by default, but they're useful.
+    enable:
+      - niliness
+      - reflectvaluecompare
+      - sortslice
+      - unusedwrite
+
+issues:
+  # Print all issues reported by all linters.
+  max-issues-per-linter: 0
+  max-same-issues: 0
+
+  # Don't ignore some of the issues that golangci-lint considers okay.
+  # This includes documenting all exported entities.
+  exclude-use-default: false
diff --git a/vendor/go.uber.org/goleak/CHANGELOG.md b/vendor/go.uber.org/goleak/CHANGELOG.md
new file mode 100644
index 000000000..5cd3f88a5
--- /dev/null
+++ b/vendor/go.uber.org/goleak/CHANGELOG.md
@@ -0,0 +1,74 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
+## [1.3.0]
+### Fixed
+- Built-in ignores now match function names more accurately.
+  They will no longer ignore stacks because of file names
+  that look similar to function names. (#112)
+### Added
+- Add an `IgnoreAnyFunction` option to ignore stack traces
+  that have the provided function anywhere in the stack. (#113)
+- Ignore `testing.runFuzzing` and `testing.runFuzzTests` alongside
+  other already-ignored test functions (`testing.RunTests`, etc). (#105)
+### Changed
+- Miscellaneous CI-related fixes. (#103, #108, #114)
+
+[1.3.0]: https://github.com/uber-go/goleak/compare/v1.2.1...v1.3.0
+
+## [1.2.1]
+### Changed
+- Drop golang/x/lint dependency.
+
+[1.2.1]: https://github.com/uber-go/goleak/compare/v1.2.0...v1.2.1
+
+## [1.2.0]
+### Added
+- Add Cleanup option that can be used for registering cleanup callbacks. (#78)
+
+### Changed
+- Mark VerifyNone as a test helper. (#75)
+
+Thanks to @tallclair for their contribution to this release.
+
+[1.2.0]: https://github.com/uber-go/goleak/compare/v1.1.12...v1.2.0
+
+## [1.1.12]
+### Fixed
+- Fixed logic for ignoring trace related goroutines on Go versions 1.16 and above.
+
+[1.1.12]: https://github.com/uber-go/goleak/compare/v1.1.11...v1.1.12
+
+## [1.1.11]
+### Fixed
+- Documentation fix on how to test.
+- Update dependency on stretchr/testify to v1.7.0. (#59)
+- Update dependency on golang.org/x/tools to address CVE-2020-14040. (#62)
+
+[1.1.11]: https://github.com/uber-go/goleak/compare/v1.1.10...v1.1.11
+
+## [1.1.10]
+### Added
+- [#49]: Add option to ignore current goroutines, which checks for any additional leaks and allows for incremental adoption of goleak in larger projects.
+
+Thanks to @denis-tingajkin for their contributions to this release.
+
+[#49]: https://github.com/uber-go/goleak/pull/49
+[1.1.10]: https://github.com/uber-go/goleak/compare/v1.0.0...v1.1.10
+
+## [1.0.0]
+### Changed
+- Migrate to Go modules.
+
+### Fixed
+- Ignore trace related goroutines that cause false positives with -trace.
+
+[1.0.0]: https://github.com/uber-go/goleak/compare/v0.10.0...v1.0.0
+
+## [0.10.0]
+- Initial release.
+
+[0.10.0]: https://github.com/uber-go/goleak/compare/v0.10.0...HEAD
diff --git a/vendor/go.uber.org/goleak/LICENSE b/vendor/go.uber.org/goleak/LICENSE
new file mode 100644
index 000000000..6c9bde216
--- /dev/null
+++ b/vendor/go.uber.org/goleak/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 Uber Technologies, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/go.uber.org/goleak/Makefile b/vendor/go.uber.org/goleak/Makefile
new file mode 100644
index 000000000..eb7154af3
--- /dev/null
+++ b/vendor/go.uber.org/goleak/Makefile
@@ -0,0 +1,45 @@
+# Directory containing the Makefile.
+PROJECT_ROOT = $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+
+export GOBIN = $(PROJECT_ROOT)/bin
+export PATH := $(GOBIN):$(PATH)
+
+GO_FILES = $(shell find . \
+	   -path '*/.*' -prune -o \
+	   '(' -type f -a -name '*.go' ')' -print)
+
+# Additional test flags.
+TEST_FLAGS ?=
+
+.PHONY: all
+all: lint build test
+
+.PHONY: lint
+lint: golangci-lint tidy-lint
+
+.PHONY: build
+build:
+	go build ./...
+
+.PHONY: test
+test:
+	go test -v -race ./...
+	go test -v -trace=/dev/null .
+
+.PHONY: cover
+cover:
+	go test -race -coverprofile=cover.out -coverpkg=./... ./...
+	go tool cover -html=cover.out -o cover.html
+
+.PHONY: golangci-lint
+golangci-lint:
+	golangci-lint run
+
+.PHONY: tidy
+tidy:
+	go mod tidy
+
+.PHONY: tidy-lint
+tidy-lint:
+	go mod tidy
+	git diff --exit-code -- go.mod go.sum
diff --git a/vendor/go.uber.org/goleak/README.md b/vendor/go.uber.org/goleak/README.md
new file mode 100644
index 000000000..de3d7d51d
--- /dev/null
+++ b/vendor/go.uber.org/goleak/README.md
@@ -0,0 +1,74 @@
+# goleak [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov]
+
+Goroutine leak detector to help avoid Goroutine leaks.
+
+## Installation
+
+You can use `go get` to get the latest version:
+
+`go get -u go.uber.org/goleak`
+
+`goleak` also supports semver releases.
+
+Note that go-leak only [supports][release] the two most recent minor versions of Go.
+
+## Quick Start
+
+To verify that there are no unexpected goroutines running at the end of a test:
+
+```go
+func TestA(t *testing.T) {
+	defer goleak.VerifyNone(t)
+
+	// test logic here.
+}
+```
+
+Instead of checking for leaks at the end of every test, `goleak` can also be run
+at the end of every test package by creating a `TestMain` function for your
+package:
+
+```go
+func TestMain(m *testing.M) {
+	goleak.VerifyTestMain(m)
+}
+```
+
+## Determine Source of Package Leaks
+
+When verifying leaks using `TestMain`, the leak test is only run once after all tests
+have been run. This is typically enough to ensure there's no goroutines leaked from
+tests, but when there are leaks, it's hard to determine which test is causing them.
+
+You can use the following bash script to determine the source of the failing test:
+
+```sh
+# Create a test binary which will be used to run each test individually
+$ go test -c -o tests
+
+# Run each test individually, printing "." for successful tests, or the test name
+# for failing tests.
+$ for test in $(go test -list . | grep -E "^(Test|Example)"); do ./tests -test.run "^$test\$" &>/dev/null && echo -n "." || echo -e "\n$test failed"; done
+```
+
+This will only print names of failing tests which can be investigated individually. E.g.,
+
+```
+.....
+TestLeakyTest failed
+.......
+```
+
+## Stability
+
+goleak is v1 and follows [SemVer](http://semver.org/) strictly.
+
+No breaking changes will be made to exported APIs before 2.0.
+
+[doc-img]: https://godoc.org/go.uber.org/goleak?status.svg
+[doc]: https://godoc.org/go.uber.org/goleak
+[ci-img]: https://github.com/uber-go/goleak/actions/workflows/ci.yml/badge.svg
+[ci]: https://github.com/uber-go/goleak/actions/workflows/ci.yml
+[cov-img]: https://codecov.io/gh/uber-go/goleak/branch/master/graph/badge.svg
+[cov]: https://codecov.io/gh/uber-go/goleak
+[release]: https://go.dev/doc/devel/release#policy
diff --git a/vendor/go.uber.org/goleak/doc.go b/vendor/go.uber.org/goleak/doc.go
new file mode 100644
index 000000000..3832f8dbc
--- /dev/null
+++ b/vendor/go.uber.org/goleak/doc.go
@@ -0,0 +1,22 @@
+// Copyright (c) 2018 Uber Technologies, Inc.
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// Package goleak is a Goroutine leak detector.
+package goleak // import "go.uber.org/goleak"
diff --git a/vendor/go.uber.org/goleak/internal/stack/doc.go b/vendor/go.uber.org/goleak/internal/stack/doc.go
new file mode 100644
index 000000000..9179a5654
--- /dev/null
+++ b/vendor/go.uber.org/goleak/internal/stack/doc.go
@@ -0,0 +1,22 @@
+// Copyright (c) 2017-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// Package stack is used for parsing stacks from `runtime.Stack`.
+package stack
diff --git a/vendor/go.uber.org/goleak/internal/stack/scan.go b/vendor/go.uber.org/goleak/internal/stack/scan.go
new file mode 100644
index 000000000..4b7ac8423
--- /dev/null
+++ b/vendor/go.uber.org/goleak/internal/stack/scan.go
@@ -0,0 +1,56 @@
+// Copyright (c) 2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package stack
+
+import (
+	"bufio"
+	"io"
+)
+
+// scanner provides a bufio.Scanner the ability to Unscan,
+// which allows the current token to be read again
+// after the next Scan.
+type scanner struct {
+	*bufio.Scanner
+
+	unscanned bool
+}
+
+func newScanner(r io.Reader) *scanner {
+	return &scanner{Scanner: bufio.NewScanner(r)}
+}
+
+func (s *scanner) Scan() bool {
+	if s.unscanned {
+		s.unscanned = false
+		return true
+	}
+	return s.Scanner.Scan()
+}
+
+// Unscan stops the scanner from advancing its position
+// for the next Scan.
+//
+// Bytes and Text will return the same token after next Scan
+// that they do right now.
+func (s *scanner) Unscan() {
+	s.unscanned = true
+}
diff --git a/vendor/go.uber.org/goleak/internal/stack/stacks.go b/vendor/go.uber.org/goleak/internal/stack/stacks.go
new file mode 100644
index 000000000..241a9b844
--- /dev/null
+++ b/vendor/go.uber.org/goleak/internal/stack/stacks.go
@@ -0,0 +1,298 @@
+// Copyright (c) 2017-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package stack
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"runtime"
+	"strconv"
+	"strings"
+)
+
+const _defaultBufferSize = 64 * 1024 // 64 KiB
+
+// Stack represents a single Goroutine's stack.
+type Stack struct {
+	id    int
+	state string // e.g. 'running', 'chan receive'
+
+	// The first function on the stack.
+	firstFunction string
+
+	// A set of all functions in the stack,
+	allFunctions map[string]struct{}
+
+	// Full, raw stack trace.
+	fullStack string
+}
+
+// ID returns the goroutine ID.
+func (s Stack) ID() int {
+	return s.id
+}
+
+// State returns the Goroutine's state.
+func (s Stack) State() string {
+	return s.state
+}
+
+// Full returns the full stack trace for this goroutine.
+func (s Stack) Full() string {
+	return s.fullStack
+}
+
+// FirstFunction returns the name of the first function on the stack.
+func (s Stack) FirstFunction() string {
+	return s.firstFunction
+}
+
+// HasFunction reports whether the stack has the given function
+// anywhere in it.
+func (s Stack) HasFunction(name string) bool {
+	_, ok := s.allFunctions[name]
+	return ok
+}
+
+func (s Stack) String() string {
+	return fmt.Sprintf(
+		"Goroutine %v in state %v, with %v on top of the stack:\n%s",
+		s.id, s.state, s.firstFunction, s.Full())
+}
+
+func getStacks(all bool) []Stack {
+	trace := getStackBuffer(all)
+	stacks, err := newStackParser(bytes.NewReader(trace)).Parse()
+	if err != nil {
+		// Well-formed stack traces should never fail to parse.
+		// If they do, it's a bug in this package.
+		// Panic so we can fix it.
+		panic(fmt.Sprintf("Failed to parse stack trace: %v\n%s", err, trace))
+	}
+	return stacks
+}
+
+type stackParser struct {
+	scan   *scanner
+	stacks []Stack
+	errors []error
+}
+
+func newStackParser(r io.Reader) *stackParser {
+	return &stackParser{
+		scan: newScanner(r),
+	}
+}
+
+func (p *stackParser) Parse() ([]Stack, error) {
+	for p.scan.Scan() {
+		line := p.scan.Text()
+
+		// If we see the goroutine header, start a new stack.
+		if strings.HasPrefix(line, "goroutine ") {
+			stack, err := p.parseStack(line)
+			if err != nil {
+				p.errors = append(p.errors, err)
+				continue
+			}
+			p.stacks = append(p.stacks, stack)
+		}
+	}
+
+	p.errors = append(p.errors, p.scan.Err())
+	return p.stacks, errors.Join(p.errors...)
+}
+
+// parseStack parses a single stack trace from the given scanner.
+// line is the first line of the stack trace, which should look like:
+//
+//	goroutine 123 [runnable]:
+func (p *stackParser) parseStack(line string) (Stack, error) {
+	id, state, err := parseGoStackHeader(line)
+	if err != nil {
+		return Stack{}, fmt.Errorf("parse header: %w", err)
+	}
+
+	// Read the rest of the stack trace.
+	var (
+		firstFunction string
+		fullStack     bytes.Buffer
+	)
+	funcs := make(map[string]struct{})
+	for p.scan.Scan() {
+		line := p.scan.Text()
+		if strings.HasPrefix(line, "goroutine ") {
+			// If we see the goroutine header,
+			// it's the end of this stack.
+			// Unscan so the next Scan sees the same line.
+			p.scan.Unscan()
+			break
+		}
+
+		fullStack.WriteString(line)
+		fullStack.WriteByte('\n') // scanner trims the newline
+
+		if len(line) == 0 {
+			// Empty line usually marks the end of the stack
+			// but we don't want to have to rely on that.
+			// Just skip it.
+			continue
+		}
+
+		funcName, creator, err := parseFuncName(line)
+		if err != nil {
+			return Stack{}, fmt.Errorf("parse function: %w", err)
+		}
+		if !creator {
+			// A function is part of a goroutine's stack
+			// only if it's not a "created by" function.
+			//
+			// The creator function is part of a different stack.
+			// We don't care about it right now.
+			funcs[funcName] = struct{}{}
+			if firstFunction == "" {
+				firstFunction = funcName
+			}
+
+		}
+
+		// The function name followed by a line in the form:
+		//
+		//	<tab>example.com/path/to/package/file.go:123 +0x123
+		//
+		// We don't care about the position so we can skip this line.
+		if p.scan.Scan() {
+			// Be defensive:
+			// Skip the line only if it starts with a tab.
+			bs := p.scan.Bytes()
+			if len(bs) > 0 && bs[0] == '\t' {
+				fullStack.Write(bs)
+				fullStack.WriteByte('\n')
+			} else {
+				// Put it back and let the next iteration handle it
+				// if it doesn't start with a tab.
+				p.scan.Unscan()
+			}
+		}
+
+		if creator {
+			// The "created by" line is the last line of the stack.
+			// We can stop parsing now.
+			//
+			// Note that if tracebackancestors=N is set,
+			// there may be more a traceback of the creator function
+			// following the "created by" line,
+			// but it should not be considered part of this stack.
+			// e.g.,
+			//
+			// created by testing.(*T).Run in goroutine 1
+			//         /usr/lib/go/src/testing/testing.go:1648 +0x3ad
+			// [originating from goroutine 1]:
+			// testing.(*T).Run(...)
+			//         /usr/lib/go/src/testing/testing.go:1649 +0x3ad
+			//
+			break
+		}
+	}
+
+	return Stack{
+		id:            id,
+		state:         state,
+		firstFunction: firstFunction,
+		allFunctions:  funcs,
+		fullStack:     fullStack.String(),
+	}, nil
+}
+
+// All returns the stacks for all running goroutines.
+func All() []Stack {
+	return getStacks(true)
+}
+
+// Current returns the stack for the current goroutine.
+func Current() Stack {
+	return getStacks(false)[0]
+}
+
+func getStackBuffer(all bool) []byte {
+	for i := _defaultBufferSize; ; i *= 2 {
+		buf := make([]byte, i)
+		if n := runtime.Stack(buf, all); n < i {
+			return buf[:n]
+		}
+	}
+}
+
+// Parses a single function from the given line.
+// The line is in one of these formats:
+//
+//	example.com/path/to/package.funcName(args...)
+//	example.com/path/to/package.(*typeName).funcName(args...)
+//	created by example.com/path/to/package.funcName
+//	created by example.com/path/to/package.funcName in goroutine [...]
+//
+// Also reports whether the line was a "created by" line.
+func parseFuncName(line string) (name string, creator bool, err error) {
+	if after, ok := strings.CutPrefix(line, "created by "); ok {
+		// The function name is the part after "created by "
+		// and before " in goroutine [...]".
+		idx := strings.Index(after, " in goroutine")
+		if idx >= 0 {
+			after = after[:idx]
+		}
+		name = after
+		creator = true
+	} else if idx := strings.LastIndexByte(line, '('); idx >= 0 {
+		// The function name is the part before the last '('.
+		name = line[:idx]
+	}
+
+	if name == "" {
+		return "", false, fmt.Errorf("no function found: %q", line)
+	}
+
+	return name, creator, nil
+}
+
+// parseGoStackHeader parses a stack header that looks like:
+// goroutine 643 [runnable]:\n
+// And returns the goroutine ID, and the state.
+func parseGoStackHeader(line string) (goroutineID int, state string, err error) {
+	// The scanner will have already trimmed the "\n",
+	// but we'll guard against it just in case.
+	//
+	// Trimming them separately makes them both optional.
+	line = strings.TrimSuffix(strings.TrimSuffix(line, ":"), "\n")
+	parts := strings.SplitN(line, " ", 3)
+	if len(parts) != 3 {
+		return 0, "", fmt.Errorf("unexpected format: %q", line)
+	}
+
+	id, err := strconv.Atoi(parts[1])
+	if err != nil {
+		return 0, "", fmt.Errorf("bad goroutine ID %q in line %q", parts[1], line)
+	}
+
+	state = strings.TrimSuffix(strings.TrimPrefix(parts[2], "["), "]")
+	return id, state, nil
+}
diff --git a/vendor/go.uber.org/goleak/leaks.go b/vendor/go.uber.org/goleak/leaks.go
new file mode 100644
index 000000000..cc206f181
--- /dev/null
+++ b/vendor/go.uber.org/goleak/leaks.go
@@ -0,0 +1,108 @@
+// Copyright (c) 2017 Uber Technologies, Inc.
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package goleak
+
+import (
+	"errors"
+	"fmt"
+
+	"go.uber.org/goleak/internal/stack"
+)
+
+// TestingT is the minimal subset of testing.TB that we use.
+type TestingT interface {
+	Error(...interface{})
+}
+
+// filterStacks will filter any stacks excluded by the given opts.
+// filterStacks modifies the passed in stacks slice.
+func filterStacks(stacks []stack.Stack, skipID int, opts *opts) []stack.Stack {
+	filtered := stacks[:0]
+	for _, stack := range stacks {
+		// Always skip the running goroutine.
+		if stack.ID() == skipID {
+			continue
+		}
+		// Run any default or user-specified filters.
+		if opts.filter(stack) {
+			continue
+		}
+		filtered = append(filtered, stack)
+	}
+	return filtered
+}
+
+// Find looks for extra goroutines, and returns a descriptive error if
+// any are found.
+func Find(options ...Option) error {
+	cur := stack.Current().ID()
+
+	opts := buildOpts(options...)
+	if opts.cleanup != nil {
+		return errors.New("Cleanup can only be passed to VerifyNone or VerifyTestMain")
+	}
+	var stacks []stack.Stack
+	retry := true
+	for i := 0; retry; i++ {
+		stacks = filterStacks(stack.All(), cur, opts)
+
+		if len(stacks) == 0 {
+			return nil
+		}
+		retry = opts.retry(i)
+	}
+
+	return fmt.Errorf("found unexpected goroutines:\n%s", stacks)
+}
+
+type testHelper interface {
+	Helper()
+}
+
+// VerifyNone marks the given TestingT as failed if any extra goroutines are
+// found by Find. This is a helper method to make it easier to integrate in
+// tests by doing:
+//
+//	defer VerifyNone(t)
+//
+// VerifyNone is currently incompatible with t.Parallel because it cannot
+// associate specific goroutines with specific tests. Thus, non-leaking
+// goroutines from other tests running in parallel could fail this check.
+// If you need to run tests in parallel, use [VerifyTestMain] instead,
+// which will verify that no leaking goroutines exist after ALL tests finish.
+func VerifyNone(t TestingT, options ...Option) {
+	opts := buildOpts(options...)
+	var cleanup func(int)
+	cleanup, opts.cleanup = opts.cleanup, nil
+
+	if h, ok := t.(testHelper); ok {
+		// Mark this function as a test helper, if available.
+		h.Helper()
+	}
+
+	if err := Find(opts); err != nil {
+		t.Error(err)
+	}
+
+	if cleanup != nil {
+		cleanup(0)
+	}
+}
diff --git a/vendor/go.uber.org/goleak/options.go b/vendor/go.uber.org/goleak/options.go
new file mode 100644
index 000000000..53fc0a1de
--- /dev/null
+++ b/vendor/go.uber.org/goleak/options.go
@@ -0,0 +1,198 @@
+// Copyright (c) 2017-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package goleak
+
+import (
+	"strings"
+	"time"
+
+	"go.uber.org/goleak/internal/stack"
+)
+
+// Option lets users specify custom verifications.
+type Option interface {
+	apply(*opts)
+}
+
+// We retry up to 20 times if we can't find the goroutine that
+// we are looking for. In between each attempt, we will sleep for
+// a short while to let any running goroutines complete.
+const _defaultRetries = 20
+
+type opts struct {
+	filters    []func(stack.Stack) bool
+	maxRetries int
+	maxSleep   time.Duration
+	cleanup    func(int)
+}
+
+// implement apply so that opts struct itself can be used as
+// an Option.
+func (o *opts) apply(opts *opts) {
+	opts.filters = o.filters
+	opts.maxRetries = o.maxRetries
+	opts.maxSleep = o.maxSleep
+	opts.cleanup = o.cleanup
+}
+
+// optionFunc lets us easily write options without a custom type.
+type optionFunc func(*opts)
+
+func (f optionFunc) apply(opts *opts) { f(opts) }
+
+// IgnoreTopFunction ignores any goroutines where the specified function
+// is at the top of the stack. The function name should be fully qualified,
+// e.g., go.uber.org/goleak.IgnoreTopFunction
+func IgnoreTopFunction(f string) Option {
+	return addFilter(func(s stack.Stack) bool {
+		return s.FirstFunction() == f
+	})
+}
+
+// IgnoreAnyFunction ignores goroutines where the specified function
+// is present anywhere in the stack.
+//
+// The function name must be fully qualified, e.g.,
+//
+//	go.uber.org/goleak.IgnoreAnyFunction
+//
+// For methods, the fully qualified form looks like:
+//
+//	go.uber.org/goleak.(*MyType).MyMethod
+func IgnoreAnyFunction(f string) Option {
+	return addFilter(func(s stack.Stack) bool {
+		return s.HasFunction(f)
+	})
+}
+
+// Cleanup sets up a cleanup function that will be executed at the
+// end of the leak check.
+// When passed to [VerifyTestMain], the exit code passed to cleanupFunc
+// will be set to the exit code of TestMain.
+// When passed to [VerifyNone], the exit code will be set to 0.
+// This cannot be passed to [Find].
+func Cleanup(cleanupFunc func(exitCode int)) Option {
+	return optionFunc(func(opts *opts) {
+		opts.cleanup = cleanupFunc
+	})
+}
+
+// IgnoreCurrent records all current goroutines when the option is created, and ignores
+// them in any future Find/Verify calls.
+func IgnoreCurrent() Option {
+	excludeIDSet := map[int]bool{}
+	for _, s := range stack.All() {
+		excludeIDSet[s.ID()] = true
+	}
+	return addFilter(func(s stack.Stack) bool {
+		return excludeIDSet[s.ID()]
+	})
+}
+
+func maxSleep(d time.Duration) Option {
+	return optionFunc(func(opts *opts) {
+		opts.maxSleep = d
+	})
+}
+
+func addFilter(f func(stack.Stack) bool) Option {
+	return optionFunc(func(opts *opts) {
+		opts.filters = append(opts.filters, f)
+	})
+}
+
+func buildOpts(options ...Option) *opts {
+	opts := &opts{
+		maxRetries: _defaultRetries,
+		maxSleep:   100 * time.Millisecond,
+	}
+	opts.filters = append(opts.filters,
+		isTestStack,
+		isSyscallStack,
+		isStdLibStack,
+		isTraceStack,
+	)
+	for _, option := range options {
+		option.apply(opts)
+	}
+	return opts
+}
+
+func (o *opts) filter(s stack.Stack) bool {
+	for _, filter := range o.filters {
+		if filter(s) {
+			return true
+		}
+	}
+	return false
+}
+
+func (o *opts) retry(i int) bool {
+	if i >= o.maxRetries {
+		return false
+	}
+
+	d := time.Duration(int(time.Microsecond) << uint(i))
+	if d > o.maxSleep {
+		d = o.maxSleep
+	}
+	time.Sleep(d)
+	return true
+}
+
+// isTestStack is a default filter installed to automatically skip goroutines
+// that the testing package runs while the user's tests are running.
+func isTestStack(s stack.Stack) bool {
+	// Until go1.7, the main goroutine ran RunTests, which started
+	// the test in a separate goroutine and waited for that test goroutine
+	// to end by waiting on a channel.
+	// Since go1.7, a separate goroutine is started to wait for signals.
+	// T.Parallel is for parallel tests, which are blocked until all serial
+	// tests have run with T.Parallel at the top of the stack.
+	// testing.runFuzzTests is for fuzz testing, it's blocked until the test
+	// function with all seed corpus have run.
+	// testing.runFuzzing is for fuzz testing, it's blocked until a failing
+	// input is found.
+	switch s.FirstFunction() {
+	case "testing.RunTests", "testing.(*T).Run", "testing.(*T).Parallel", "testing.runFuzzing", "testing.runFuzzTests":
+		// In pre1.7 and post-1.7, background goroutines started by the testing
+		// package are blocked waiting on a channel.
+		return strings.HasPrefix(s.State(), "chan receive")
+	}
+	return false
+}
+
+func isSyscallStack(s stack.Stack) bool {
+	// Typically runs in the background when code uses CGo:
+	// https://github.com/golang/go/issues/16714
+	return s.HasFunction("runtime.goexit") && strings.HasPrefix(s.State(), "syscall")
+}
+
+func isStdLibStack(s stack.Stack) bool {
+	// Importing os/signal starts a background goroutine.
+	// The name of the function at the top has changed between versions.
+	if f := s.FirstFunction(); f == "os/signal.signal_recv" || f == "os/signal.loop" {
+		return true
+	}
+
+	// Using signal.Notify will start a runtime goroutine.
+	return s.HasFunction("runtime.ensureSigM")
+}
diff --git a/vendor/go.uber.org/goleak/tracestack_new.go b/vendor/go.uber.org/goleak/tracestack_new.go
new file mode 100644
index 000000000..4fc6cefce
--- /dev/null
+++ b/vendor/go.uber.org/goleak/tracestack_new.go
@@ -0,0 +1,30 @@
+// Copyright (c) 2021-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+//go:build go1.16
+// +build go1.16
+
+package goleak
+
+import "go.uber.org/goleak/internal/stack"
+
+func isTraceStack(s stack.Stack) bool {
+	return s.HasFunction("runtime.ReadTrace")
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 98850894e..5596fbfec 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -558,6 +558,10 @@ go.opentelemetry.io/proto/otlp/collector/trace/v1
 go.opentelemetry.io/proto/otlp/common/v1
 go.opentelemetry.io/proto/otlp/resource/v1
 go.opentelemetry.io/proto/otlp/trace/v1
+# go.uber.org/goleak v1.3.0
+## explicit; go 1.20
+go.uber.org/goleak
+go.uber.org/goleak/internal/stack
 # go.uber.org/mock v0.4.0
 ## explicit; go 1.20
 go.uber.org/mock/gomock


Test patch:
diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go
index 969958614..13548e73e 100644
--- a/pkg/util/util_test.go
+++ b/pkg/util/util_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 	"time"
 
+	"go.uber.org/goleak"
 	gomock "go.uber.org/mock/gomock"
 )
 
@@ -351,6 +352,7 @@ func TestParseAzcopyJobShow(t *testing.T) {
 }
 
 func TestWaitUntilTimeout(t *testing.T) {
+	defer goleak.VerifyNone(t)
 	tests := []struct {
 		desc        string
 		timeout     time.Duration
@@ -377,6 +379,7 @@ func TestWaitUntilTimeout(t *testing.T) {
 				return nil
 			},
 			timeoutFunc: func() error {
+				time.Sleep(2 * time.Second)
 				return fmt.Errorf("timeout error")
 			},
 			expectedErr: fmt.Errorf("timeout error"),
diff --git a/vendor/go.uber.org/goleak/testmain.go b/vendor/go.uber.org/goleak/testmain.go
new file mode 100644
index 000000000..7b1a50b7a
--- /dev/null
+++ b/vendor/go.uber.org/goleak/testmain.go
@@ -0,0 +1,69 @@
+// Copyright (c) 2017 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package goleak
+
+import (
+	"fmt"
+	"io"
+	"os"
+)
+
+// Variables for stubbing in unit tests.
+var (
+	_osExit             = os.Exit
+	_osStderr io.Writer = os.Stderr
+)
+
+// TestingM is the minimal subset of testing.M that we use.
+type TestingM interface {
+	Run() int
+}
+
+// VerifyTestMain can be used in a TestMain function for package tests to
+// verify that there were no goroutine leaks.
+// To use it, your TestMain function should look like:
+//
+//	func TestMain(m *testing.M) {
+//	  goleak.VerifyTestMain(m)
+//	}
+//
+// See https://golang.org/pkg/testing/#hdr-Main for more details.
+//
+// This will run all tests as per normal, and if they were successful, look
+// for any goroutine leaks and fail the tests if any leaks were found.
+func VerifyTestMain(m TestingM, options ...Option) {
+	exitCode := m.Run()
+	opts := buildOpts(options...)
+
+	var cleanup func(int)
+	cleanup, opts.cleanup = opts.cleanup, nil
+	if cleanup == nil {
+		cleanup = _osExit
+	}
+	defer func() { cleanup(exitCode) }()
+
+	if exitCode == 0 {
+		if err := Find(opts); err != nil {
+			fmt.Fprintf(_osStderr, "goleak: Errors on successful test run: %v\n", err)
+			exitCode = 1
+		}
+	}
+}