From 5928e851dd00279f3a9d4756f374842e8ae879ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:24:57 +0000 Subject: [PATCH] Bump github.com/sasha-s/go-deadlock from 0.3.6 to 0.3.9 Bumps [github.com/sasha-s/go-deadlock](https://github.com/sasha-s/go-deadlock) from 0.3.6 to 0.3.9. - [Release notes](https://github.com/sasha-s/go-deadlock/releases) - [Commits](https://github.com/sasha-s/go-deadlock/compare/v0.3.6...v0.3.9) --- updated-dependencies: - dependency-name: github.com/sasha-s/go-deadlock dependency-version: 0.3.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 +- .../github.com/sasha-s/go-deadlock/Readme.md | 35 +- .../sasha-s/go-deadlock/beforeafter_go124.go | 30 ++ .../sasha-s/go-deadlock/beforeafter_legacy.go | 12 + .../sasha-s/go-deadlock/deadlock.go | 411 ++++++++++++------ .../sasha-s/go-deadlock/deadlock_map.go | 1 + .../github.com/sasha-s/go-deadlock/mutex.go | 21 + .../sasha-s/go-deadlock/stacktraces.go | 35 +- .../sasha-s/go-deadlock/sync_disable.go | 55 +++ .../sasha-s/go-deadlock/sync_disable_go118.go | 55 +++ .../sasha-s/go-deadlock/sync_mutex_go118.go | 65 +++ .../sasha-s/go-deadlock/sync_mutex_legacy.go | 68 +++ .../sasha-s/go-deadlock/synctest_mutex.go | 234 ++++++++++ .../sasha-s/go-deadlock/timerpool_default.go | 18 + .../sasha-s/go-deadlock/timerpool_disable.go | 9 + .../sasha-s/go-deadlock/timerpool_go125.go | 18 + .../sasha-s/go-deadlock/timerpool_synctest.go | 18 + .../github.com/sasha-s/go-deadlock/trylock.go | 10 +- vendor/modules.txt | 2 +- 20 files changed, 963 insertions(+), 140 deletions(-) create mode 100644 vendor/github.com/sasha-s/go-deadlock/beforeafter_go124.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/beforeafter_legacy.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/mutex.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/sync_disable.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/sync_disable_go118.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/sync_mutex_go118.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/sync_mutex_legacy.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/synctest_mutex.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/timerpool_default.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/timerpool_disable.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/timerpool_go125.go create mode 100644 vendor/github.com/sasha-s/go-deadlock/timerpool_synctest.go diff --git a/go.mod b/go.mod index 74dfa0c7a..fb8989e71 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/sahilm/fuzzy v0.1.1 github.com/samber/lo v1.31.0 github.com/sanity-io/litter v1.5.8 - github.com/sasha-s/go-deadlock v0.3.6 + github.com/sasha-s/go-deadlock v0.3.9 github.com/sirupsen/logrus v1.9.3 github.com/spf13/afero v1.15.0 github.com/spkg/bom v1.0.1 diff --git a/go.sum b/go.sum index 7a7be43f2..f6af75461 100644 --- a/go.sum +++ b/go.sum @@ -109,8 +109,8 @@ github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM= github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= -github.com/sasha-s/go-deadlock v0.3.6 h1:TR7sfOnZ7x00tWPfD397Peodt57KzMDo+9Ae9rMiUmw= -github.com/sasha-s/go-deadlock v0.3.6/go.mod h1:CUqNyyvMxTyjFqDT7MRg9mb4Dv/btmGTqSR+rky/UXo= +github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w= +github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= diff --git a/vendor/github.com/sasha-s/go-deadlock/Readme.md b/vendor/github.com/sasha-s/go-deadlock/Readme.md index a1eb793c0..0a12ea5cd 100644 --- a/vendor/github.com/sasha-s/go-deadlock/Readme.md +++ b/vendor/github.com/sasha-s/go-deadlock/Readme.md @@ -1,4 +1,4 @@ -# Online deadlock detection in go (golang). [![Try it online](https://img.shields.io/badge/try%20it-online-blue.svg)](https://wandbox.org/permlink/hJc6QCZowxbNm9WW) [![Docs](https://godoc.org/github.com/sasha-s/go-deadlock?status.svg)](https://godoc.org/github.com/sasha-s/go-deadlock) [![codecov](https://codecov.io/gh/sasha-s/go-deadlock/branch/master/graph/badge.svg)](https://codecov.io/gh/sasha-s/go-deadlock) [![version](https://badge.fury.io/gh/sasha-s%2Fgo-deadlock.svg)](https://github.com/sasha-s/go-deadlock/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/sasha-s/go-deadlock)](https://goreportcard.com/report/github.com/sasha-s/go-deadlock) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +# Online deadlock detection in go (golang). [![Try it online](https://img.shields.io/badge/try%20it-online-blue.svg)](https://wandbox.org/permlink/hJc6QCZowxbNm9WW) [![Docs](https://godoc.org/github.com/sasha-s/go-deadlock?status.svg)](https://godoc.org/github.com/sasha-s/go-deadlock) [![codecov](https://codecov.io/gh/sasha-s/go-deadlock/branch/main/graph/badge.svg)](https://codecov.io/gh/sasha-s/go-deadlock) [![version](https://badge.fury.io/gh/sasha-s%2Fgo-deadlock.svg)](https://github.com/sasha-s/go-deadlock/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/sasha-s/go-deadlock)](https://goreportcard.com/report/github.com/sasha-s/go-deadlock) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ## Why Deadlocks happen and are painful to debug. @@ -172,16 +172,45 @@ func main() { rlockTwice() } ``` +## Build Tags and Compatibility Modes + +go-deadlock supports multiple build configurations for different use cases: + +* **Normal mode** (default): Full deadlock detection with timer pooling +* **Synctest mode**: Compatible with Go's `testing/synctest` package - use either: + * `-tags=deadlock_synctest` (recommended for Go 1.25+) + * `-tags=goexperiment.synctest` (for experimental synctest) +* **Disabled mode** (`-tags=deadlock_disable`): Zero overhead, no detection + +**Why synctest mode?** `sync.Mutex` is not durably blocking in synctest bubbles, but channels are. Synctest mode uses channel-based mutexes to ensure proper behavior with `testing/synctest`. + +### Quick Examples + +```bash +# Normal development/testing +go test ./... + +# Testing with synctest (Go 1.25+, recommended) +GODEBUG=asynctimerchan=0 go test -tags=deadlock_synctest ./... + +# Testing with experimental synctest +GODEBUG=asynctimerchan=0 go test -tags=goexperiment.synctest ./... + +# Production build with zero overhead +go build -tags=deadlock_disable ./... +``` + ## Configuring go-deadlock Have a look at [Opts](https://pkg.go.dev/github.com/sasha-s/go-deadlock#pkg-variables). -* `Opts.Disable`: disables deadlock detection altogether +* `Opts.Disable`: disables deadlock detection altogether (runtime option; see also `deadlock_disable` build tag) * `Opts.DisableLockOrderDetection`: disables lock order based deadlock detection. * `Opts.DeadlockTimeout`: blocking on mutex for longer than DeadlockTimeout is considered a deadlock. ignored if negative * `Opts.OnPotentialDeadlock`: callback for then deadlock is detected * `Opts.MaxMapSize`: size of happens before // happens after table * `Opts.PrintAllCurrentGoroutines`: dump stacktraces of all goroutines when inconsistent locking is detected, verbose * `Opts.LogBuf`: where to write deadlock info/stacktraces +* `Opts.TimerPool`: controls timer pooling behavior (auto-configured based on build tags) + - diff --git a/vendor/github.com/sasha-s/go-deadlock/beforeafter_go124.go b/vendor/github.com/sasha-s/go-deadlock/beforeafter_go124.go new file mode 100644 index 000000000..6bb1e4e52 --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/beforeafter_go124.go @@ -0,0 +1,30 @@ +//go:build go1.24 + +package deadlock + +import ( + "unsafe" + "weak" +) + +type beforeAfter struct { + before weak.Pointer[byte] + after weak.Pointer[byte] +} + +// ptrFromInterface extracts the data pointer from an interface{} value. +// An interface (eface) is {type *_type, data unsafe.Pointer}; we grab the second word. +func ptrFromInterface(i interface{}) *byte { + type eface struct { + _ uintptr + data unsafe.Pointer + } + return (*byte)((*eface)(unsafe.Pointer(&i)).data) +} + +func newBeforeAfter(before, after interface{}) beforeAfter { + return beforeAfter{ + before: weak.Make(ptrFromInterface(before)), + after: weak.Make(ptrFromInterface(after)), + } +} diff --git a/vendor/github.com/sasha-s/go-deadlock/beforeafter_legacy.go b/vendor/github.com/sasha-s/go-deadlock/beforeafter_legacy.go new file mode 100644 index 000000000..0b8912b82 --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/beforeafter_legacy.go @@ -0,0 +1,12 @@ +//go:build !go1.24 + +package deadlock + +type beforeAfter struct { + before interface{} + after interface{} +} + +func newBeforeAfter(before, after interface{}) beforeAfter { + return beforeAfter{before: before, after: after} +} diff --git a/vendor/github.com/sasha-s/go-deadlock/deadlock.go b/vendor/github.com/sasha-s/go-deadlock/deadlock.go index a285c751d..33766b8a4 100644 --- a/vendor/github.com/sasha-s/go-deadlock/deadlock.go +++ b/vendor/github.com/sasha-s/go-deadlock/deadlock.go @@ -7,11 +7,24 @@ import ( "io" "os" "sync" + "sync/atomic" "time" "github.com/petermattis/goid" ) +// TimerPoolMode controls timer pooling behavior +type TimerPoolMode int + +const ( + // TimerPoolDefault automatically chooses based on build environment + TimerPoolDefault TimerPoolMode = iota + // TimerPoolEnabled always uses timer pooling for performance + TimerPoolEnabled + // TimerPoolDisabled disables timer pooling (required for testing/synctest) + TimerPoolDisabled +) + // Opts control how deadlock detection behaves. // Options are supposed to be set once at a startup (say, when parsing flags). var Opts = struct { @@ -31,7 +44,12 @@ var Opts = struct { MaxMapSize int // Will dump stacktraces of all goroutines when inconsistent locking is detected. PrintAllCurrentGoroutines bool - mu *sync.Mutex // Protects the LogBuf. + // Controls timer pooling behavior. + // TimerPoolDefault: Automatically choose based on build environment + // TimerPoolEnabled: Always use timer pooling + // TimerPoolDisabled: Never use timer pooling + TimerPool TimerPoolMode + mu *sync.Mutex // Protects the LogBuf. // Will print deadlock info to log buffer. LogBuf io.Writer }{ @@ -75,7 +93,7 @@ var NewCond = sync.NewCond // A Mutex is a drop-in replacement for sync.Mutex. // Performs deadlock detection unless disabled in Opts. type Mutex struct { - mu sync.Mutex + mu StandardMutex } // Lock locks the mutex. @@ -104,7 +122,7 @@ func (m *Mutex) Unlock() { // An RWMutex is a drop-in replacement for sync.RWMutex. // Performs deadlock detection unless disabled in Opts. type RWMutex struct { - mu sync.RWMutex + mu StandardRWMutex } // Lock locks rw for writing. @@ -155,15 +173,15 @@ func (m *RWMutex) RUnlock() { // RLocker returns a Locker interface that implements // the Lock and Unlock methods by calling RLock and RUnlock. func (m *RWMutex) RLocker() sync.Locker { - return (*rlocker)(m) + return m.mu.RLocker() } func preLock(stack []uintptr, p interface{}) { lo.preLock(stack, p) } -func postLock(stack []uintptr, p interface{}) { - lo.postLock(stack, p) +func postLock(stack []uintptr, buf *[stackBufSize]uintptr, p interface{}) { + lo.postLock(stack, buf, p) } func postUnlock(p interface{}) { @@ -175,104 +193,180 @@ func lock(lockFn func(), ptr interface{}) { lockFn() return } - stack := callers(1) + stack, buf := callers(1) + // Cache timeout before preLock so all Opts reads complete before preLock + // may call OnPotentialDeadlock. If preLock detects a problem (recursive + // lock, order violation) the goroutine may block forever in lockFn below, + // and reading Opts after preLock would race with any later Opts write. + timeout := Opts.DeadlockTimeout preLock(stack, ptr) - if Opts.DeadlockTimeout <= 0 { + if timeout <= 0 { lockFn() } else { - ch := make(chan struct{}) currentID := goid.Get() - go checkDeadlock(stack, ptr, currentID, ch) + e := dw.register(stack, ptr, currentID, timeout) lockFn() - postLock(stack, ptr) - close(ch) + dw.deregister(e) + postLock(stack, buf, ptr) return } - postLock(stack, ptr) + postLock(stack, buf, ptr) } -var timersPool sync.Pool - -func acquireTimer(d time.Duration) *time.Timer { - t, ok := timersPool.Get().(*time.Timer) - if ok { - _ = t.Reset(d) - return t - } - return time.NewTimer(Opts.DeadlockTimeout) +// pendingEntry tracks a goroutine that is waiting to acquire a lock. Entries are +// pooled to avoid per-lock heap allocations (goroutine stacks, channels, closures). +// +// Timer safety invariants: +// - checkFn is allocated once per entry and reused across pool cycles, so recycling +// an entry does not allocate a new closure. +// - The done flag synchronizes the callback with deregister: deregister sets done=1 +// before calling Stop(), and the callback checks done before acting. Because both +// use atomic operations, the callback is guaranteed to observe done=1 if deregister +// has already run, even if the runtime already scheduled the callback. +// - An entry is only returned to the pool when timer.Stop() returns true, meaning +// the timer was successfully cancelled and the callback will never run. This prevents +// a recycled entry from being mutated by an in-flight callback. +// - When Stop() returns false (callback already firing or queued), the entry is +// intentionally leaked to GC. This only happens in the rare deadlock-timeout path. +type pendingEntry struct { + stack []uintptr + ptr interface{} + gid int64 + done int32 // atomic: 0=pending, 1=acquired + timer *time.Timer + checkFn func() } -func releaseTimer(t *time.Timer) { - if !t.Stop() { - <-t.C - } - timersPool.Put(t) -} - -func checkDeadlock(stack []uintptr, ptr interface{}, currentID int64, ch <-chan struct{}) { - t := acquireTimer(Opts.DeadlockTimeout) - defer releaseTimer(t) - for { - select { - case <-t.C: - lo.mu.Lock() - prev, ok := lo.cur[ptr] - if !ok { - lo.mu.Unlock() - break // Nobody seems to be holding the lock, try again. - } - Opts.mu.Lock() - fmt.Fprintln(Opts.LogBuf, header) - fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed") - fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", prev.gid, ptr) - printStack(Opts.LogBuf, prev.stack) - fmt.Fprintln(Opts.LogBuf, "Have been trying to lock it again for more than", Opts.DeadlockTimeout) - fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", currentID, ptr) - printStack(Opts.LogBuf, stack) - stacks := stacks() - grs := bytes.Split(stacks, []byte("\n\n")) - for _, g := range grs { - if goid.ExtractGID(g) == prev.gid { - fmt.Fprintln(Opts.LogBuf, "Here is what goroutine", prev.gid, "doing now") - Opts.LogBuf.Write(g) - fmt.Fprintln(Opts.LogBuf) - } - } - lo.other(ptr) - if Opts.PrintAllCurrentGoroutines { - fmt.Fprintln(Opts.LogBuf, "All current goroutines:") - Opts.LogBuf.Write(stacks) - } - fmt.Fprintln(Opts.LogBuf) - if buf, ok := Opts.LogBuf.(*bufio.Writer); ok { - buf.Flush() - } - Opts.mu.Unlock() - lo.mu.Unlock() - Opts.OnPotentialDeadlock() - <-ch - return - case <-ch: +func newPendingEntry() *pendingEntry { + e := &pendingEntry{} + // Capture e by pointer so the closure is stable across pool reuse, no new + // closure allocation when the entry is recycled. + e.checkFn = func() { + // If the lock was acquired (done=1), the entry may already be back in the + // pool or being reused. Bail out unconditionally. + if atomic.LoadInt32(&e.done) != 0 { return } - t.Reset(Opts.DeadlockTimeout) + onDeadlockTimeout(e) } + return e +} + +var pendingPool = sync.Pool{ + New: func() interface{} { + return newPendingEntry() + }, +} + +type deadlockWatcher struct{} + +var dw deadlockWatcher + +func (w *deadlockWatcher) register(stack []uintptr, ptr interface{}, gid int64, timeout time.Duration) *pendingEntry { + var e *pendingEntry + if shouldDisableTimerPool() { + e = newPendingEntry() + } else { + e = pendingPool.Get().(*pendingEntry) + } + e.stack = stack + e.ptr = ptr + e.gid = gid + atomic.StoreInt32(&e.done, 0) + if e.timer == nil { + // First use (freshly allocated entry): create the AfterFunc timer. + // AfterFunc avoids the channel-drain problems of channel-based timers, + // which are especially problematic under testing/synctest. + e.timer = time.AfterFunc(timeout, e.checkFn) + } else { + // Reused from pool: the timer was previously Stop()'d successfully + // (guaranteed by deregister), so Reset is safe here. + e.timer.Reset(timeout) + } + return e +} + +// deregister marks the lock as acquired and cancels the deadlock timer. +// Must be called exactly once per register call. The entry pointer is +// stack-local in lock(), so concurrent or duplicate calls cannot occur. +func (w *deadlockWatcher) deregister(e *pendingEntry) { + // Mark done BEFORE stopping the timer. The callback checks done with an + // atomic load, so even if the timer fires concurrently, the callback will + // see done=1 and return without acting. + atomic.StoreInt32(&e.done, 1) + stopped := e.timer.Stop() + // Only recycle the entry if Stop() confirmed the callback won't run. + // If Stop() returned false the callback is already executing or queued; + // recycling would race with the callback reading entry fields. + if stopped && !shouldDisableTimerPool() { + e.stack = nil + e.ptr = nil + e.gid = 0 + pendingPool.Put(e) + } +} + +func onDeadlockTimeout(e *pendingEntry) { + lo.mu.Lock() + holders, ok := lo.cur[e.ptr] + if !ok || len(holders) == 0 { + // Lock appears unheld (transient state, holder may have just released). + // Reschedule if the waiter is still pending. Note: this creates a new timer + // (e.timer is not updated), so if deregister runs later it will Stop() the + // original (already-fired) timer, get false, and skip pooling. The new timer's + // callback will then observe done=1 and no-op. This is safe but means the + // entry won't be recycled, acceptable since this is the rare timeout path. + lo.mu.Unlock() + if atomic.LoadInt32(&e.done) == 0 { + time.AfterFunc(Opts.DeadlockTimeout, e.checkFn) + } + return + } + Opts.mu.Lock() + fmt.Fprintln(Opts.LogBuf, header) + for _, prev := range holders { + fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed") + fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", prev.gid, e.ptr) + printStack(Opts.LogBuf, prev.stack) + } + fmt.Fprintln(Opts.LogBuf, "Have been trying to lock it again for more than", Opts.DeadlockTimeout) + fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", e.gid, e.ptr) + printStack(Opts.LogBuf, e.stack) + stacks := stacks() + grs := bytes.Split(stacks, []byte("\n\n")) + for _, prev := range holders { + for _, g := range grs { + if goid.ExtractGID(g) == prev.gid { + fmt.Fprintln(Opts.LogBuf, "Here is what goroutine", prev.gid, "doing now") + Opts.LogBuf.Write(g) + fmt.Fprintln(Opts.LogBuf) + } + } + } + lo.other(e.ptr) + if Opts.PrintAllCurrentGoroutines { + fmt.Fprintln(Opts.LogBuf, "All current goroutines:") + Opts.LogBuf.Write(stacks) + } + fmt.Fprintln(Opts.LogBuf) + if buf, ok := Opts.LogBuf.(*bufio.Writer); ok { + buf.Flush() + } + Opts.mu.Unlock() + lo.mu.Unlock() + Opts.OnPotentialDeadlock() } type lockOrder struct { mu sync.Mutex - cur map[interface{}]stackGID // stacktraces + gids for the locks currently taken. - order map[beforeAfter]ss // expected order of locks. + cur map[interface{}][]stackGID // stacktraces + gids for the locks currently taken. + order map[beforeAfter]ss // expected order of locks. } type stackGID struct { stack []uintptr gid int64 -} - -type beforeAfter struct { - before interface{} - after interface{} + buf *[stackBufSize]uintptr // pooled backing array; returned via releaseStackBuf in postUnlock } type ss struct { @@ -284,15 +378,31 @@ var lo = newLockOrder() func newLockOrder() *lockOrder { return &lockOrder{ - cur: map[interface{}]stackGID{}, + cur: map[interface{}][]stackGID{}, order: map[beforeAfter]ss{}, } } -func (l *lockOrder) postLock(stack []uintptr, p interface{}) { +// holdersPool recycles []stackGID slices used by lockOrder.cur to track which +// goroutines currently hold each lock. Slices are returned to the pool in +// postUnlock when a lock's holder count drops to zero, and reused in postLock +// for the next lock acquisition, avoiding a new slice allocation per mutex. +var holdersPool sync.Pool + +// postLock records the current goroutine as a holder of lock p. It tries to +// reuse a pooled []stackGID slice before allocating, and stores the pooled +// stack buffer in the entry so postUnlock can release it later. +func (l *lockOrder) postLock(stack []uintptr, buf *[stackBufSize]uintptr, p interface{}) { gid := goid.Get() + entry := stackGID{stack, gid, buf} l.mu.Lock() - l.cur[p] = stackGID{stack, gid} + holders := l.cur[p] + if holders == nil { + if s, ok := holdersPool.Get().([]stackGID); ok { + holders = s[:0] + } + } + l.cur[p] = append(holders, entry) l.mu.Unlock() } @@ -302,84 +412,131 @@ func (l *lockOrder) preLock(stack []uintptr, p interface{}) { } gid := goid.Get() l.mu.Lock() - for b, bs := range l.cur { + for b, holders := range l.cur { if b == p { - if bs.gid == gid { + for _, bs := range holders { + if bs.gid == gid { + Opts.mu.Lock() + fmt.Fprintln(Opts.LogBuf, header, "Recursive locking:") + fmt.Fprintf(Opts.LogBuf, "current goroutine %d lock %p\n", gid, b) + printStack(Opts.LogBuf, stack) + fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed (same goroutine)") + printStack(Opts.LogBuf, bs.stack) + l.other(p) + if buf, ok := Opts.LogBuf.(*bufio.Writer); ok { + buf.Flush() + } + Opts.mu.Unlock() + Opts.OnPotentialDeadlock() + break + } + } + continue + } + for _, bs := range holders { + if bs.gid != gid { // We want locks taken in the same goroutine only. + continue + } + if s, ok := l.order[newBeforeAfter(p, b)]; ok { Opts.mu.Lock() - fmt.Fprintln(Opts.LogBuf, header, "Recursive locking:") - fmt.Fprintf(Opts.LogBuf, "current goroutine %d lock %p\n", gid, b) - printStack(Opts.LogBuf, stack) - fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed (same goroutine)") + fmt.Fprintln(Opts.LogBuf, header, "Inconsistent locking. saw this ordering in one goroutine:") + fmt.Fprintln(Opts.LogBuf, "happened before") + printStack(Opts.LogBuf, s.before) + fmt.Fprintln(Opts.LogBuf, "happened after") + printStack(Opts.LogBuf, s.after) + fmt.Fprintln(Opts.LogBuf, "in another goroutine: happened before") printStack(Opts.LogBuf, bs.stack) + fmt.Fprintln(Opts.LogBuf, "happened after") + printStack(Opts.LogBuf, stack) l.other(p) + fmt.Fprintln(Opts.LogBuf) if buf, ok := Opts.LogBuf.(*bufio.Writer); ok { buf.Flush() } Opts.mu.Unlock() Opts.OnPotentialDeadlock() } - continue - } - if bs.gid != gid { // We want locks taken in the same goroutine only. - continue - } - if s, ok := l.order[beforeAfter{p, b}]; ok { - Opts.mu.Lock() - fmt.Fprintln(Opts.LogBuf, header, "Inconsistent locking. saw this ordering in one goroutine:") - fmt.Fprintln(Opts.LogBuf, "happened before") - printStack(Opts.LogBuf, s.before) - fmt.Fprintln(Opts.LogBuf, "happened after") - printStack(Opts.LogBuf, s.after) - fmt.Fprintln(Opts.LogBuf, "in another goroutine: happened before") - printStack(Opts.LogBuf, bs.stack) - fmt.Fprintln(Opts.LogBuf, "happened after") - printStack(Opts.LogBuf, stack) - l.other(p) - fmt.Fprintln(Opts.LogBuf) - if buf, ok := Opts.LogBuf.(*bufio.Writer); ok { - buf.Flush() + // Copy both stacks: they're backed by pooled buffers that will be + // recycled in postUnlock, but l.order entries persist until MaxMapSize. + l.order[newBeforeAfter(b, p)] = ss{copyStack(bs.stack), copyStack(stack)} + if len(l.order) == Opts.MaxMapSize { // Reset the map to keep memory footprint bounded. + l.order = map[beforeAfter]ss{} } - Opts.mu.Unlock() - Opts.OnPotentialDeadlock() - } - l.order[beforeAfter{b, p}] = ss{bs.stack, stack} - if len(l.order) == Opts.MaxMapSize { // Reset the map to keep memory footprint bounded. - l.order = map[beforeAfter]ss{} } } l.mu.Unlock() } func (l *lockOrder) postUnlock(p interface{}) { + gid := goid.Get() l.mu.Lock() - delete(l.cur, p) + holders := l.cur[p] + idx := -1 + for i, h := range holders { + if h.gid == gid { + idx = i + break + } + } + if idx >= 0 { + removedBuf := holders[idx].buf + holders[idx] = holders[len(holders)-1] + holders[len(holders)-1] = stackGID{} + holders = holders[:len(holders)-1] + releaseStackBuf(removedBuf) + } else if len(holders) > 0 { + // Cross-goroutine unlock: Go permits one goroutine to Lock and a different + // goroutine to Unlock, so the unlocking gid may not match any holder entry. + // This is a rare edge case in practice, the vast majority of code unlocks + // from the same goroutine that locked. We remove an arbitrary entry to keep + // the holder count consistent with the real lock state (the lock *was* + // released, so one entry must go). The trade-off: for RWMutex with multiple + // concurrent readers we may discard the wrong reader's stack trace, making a + // future deadlock report show a slightly misleading "previous lock site". + // Detection correctness is unaffected. + removedBuf := holders[len(holders)-1].buf + holders[len(holders)-1] = stackGID{} + holders = holders[:len(holders)-1] + releaseStackBuf(removedBuf) + } + if len(holders) == 0 { + // Delete the map key so the mutex pointer is not retained, allowing GC of + // the struct it's embedded in. Recycle the backing slice via pool so the + // next postLock on any mutex can reuse it instead of allocating. + if cap(holders) > 0 { + holdersPool.Put(holders[:0]) + } + delete(l.cur, p) + } else { + l.cur[p] = holders + } l.mu.Unlock() } -type rlocker RWMutex - -func (r *rlocker) Lock() { (*RWMutex)(r).RLock() } -func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() } - // Under lo.mu Locked. func (l *lockOrder) other(ptr interface{}) { empty := true - for k := range l.cur { + for k, holders := range l.cur { if k == ptr { continue } - empty = false + if len(holders) > 0 { + empty = false + break + } } if empty { return } fmt.Fprintln(Opts.LogBuf, "Other goroutines holding locks:") - for k, pp := range l.cur { + for k, holders := range l.cur { if k == ptr { continue } - fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", pp.gid, k) - printStack(Opts.LogBuf, pp.stack) + for _, pp := range holders { + fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", pp.gid, k) + printStack(Opts.LogBuf, pp.stack) + } } fmt.Fprintln(Opts.LogBuf) } diff --git a/vendor/github.com/sasha-s/go-deadlock/deadlock_map.go b/vendor/github.com/sasha-s/go-deadlock/deadlock_map.go index ec66bdc0f..70277f2bc 100644 --- a/vendor/github.com/sasha-s/go-deadlock/deadlock_map.go +++ b/vendor/github.com/sasha-s/go-deadlock/deadlock_map.go @@ -1,3 +1,4 @@ +//go:build go1.9 // +build go1.9 package deadlock diff --git a/vendor/github.com/sasha-s/go-deadlock/mutex.go b/vendor/github.com/sasha-s/go-deadlock/mutex.go new file mode 100644 index 000000000..a7d7f1c3d --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/mutex.go @@ -0,0 +1,21 @@ +package deadlock + +import "sync" + +// MutexImpl defines the interface for mutex implementations +type MutexImpl interface { + Lock() + Unlock() + TryLock() bool +} + +// RWMutexImpl defines the interface for rwmutex implementations +type RWMutexImpl interface { + Lock() + Unlock() + RLock() + RUnlock() + TryLock() bool + TryRLock() bool + RLocker() sync.Locker +} diff --git a/vendor/github.com/sasha-s/go-deadlock/stacktraces.go b/vendor/github.com/sasha-s/go-deadlock/stacktraces.go index d93050fcd..fb5f308d8 100644 --- a/vendor/github.com/sasha-s/go-deadlock/stacktraces.go +++ b/vendor/github.com/sasha-s/go-deadlock/stacktraces.go @@ -13,9 +13,38 @@ import ( "sync" ) -func callers(skip int) []uintptr { - s := make([]uintptr, 50) // Most relevant context seem to appear near the top of the stack. - return s[:runtime.Callers(2+skip, s)] +const stackBufSize = 50 + +var stackBufPool = sync.Pool{ + New: func() interface{} { + return new([stackBufSize]uintptr) + }, +} + +// callers returns a stack trace backed by a pooled buffer. The caller must +// eventually return buf via releaseStackBuf — typically through the +// postLock/postUnlock path which stores it in stackGID.buf. +func callers(skip int) ([]uintptr, *[stackBufSize]uintptr) { + buf := stackBufPool.Get().(*[stackBufSize]uintptr) + n := runtime.Callers(2+skip, buf[:]) + return buf[:n], buf +} + +// releaseStackBuf returns a pooled stack buffer obtained from callers(). Safe to +// call with nil (e.g. when the buffer was already handed off via stackGID.buf). +func releaseStackBuf(buf *[stackBufSize]uintptr) { + if buf != nil { + stackBufPool.Put(buf) + } +} + +// copyStack creates an independent copy of a stack trace. Required when storing +// stacks in long-lived structures (e.g. l.order) because the originals are backed +// by pooled buffers that will be recycled in postUnlock. +func copyStack(s []uintptr) []uintptr { + c := make([]uintptr, len(s)) + copy(c, s) + return c } func printStack(w io.Writer, stack []uintptr) { diff --git a/vendor/github.com/sasha-s/go-deadlock/sync_disable.go b/vendor/github.com/sasha-s/go-deadlock/sync_disable.go new file mode 100644 index 000000000..cd9778c50 --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/sync_disable.go @@ -0,0 +1,55 @@ +//go:build deadlock_disable && !go1.18 + +package deadlock + +import "sync" + +// StandardMutex wraps sync.Mutex with no deadlock detection +type StandardMutex struct { + mu sync.Mutex +} + +func (m *StandardMutex) Lock() { + m.mu.Lock() +} + +func (m *StandardMutex) Unlock() { + m.mu.Unlock() +} + +func (m *StandardMutex) TryLock() bool { + panic("TryLock requires Go 1.18 or later") +} + +// StandardRWMutex wraps sync.RWMutex with no deadlock detection +type StandardRWMutex struct { + mu sync.RWMutex +} + +func (m *StandardRWMutex) Lock() { + m.mu.Lock() +} + +func (m *StandardRWMutex) Unlock() { + m.mu.Unlock() +} + +func (m *StandardRWMutex) RLock() { + m.mu.RLock() +} + +func (m *StandardRWMutex) RUnlock() { + m.mu.RUnlock() +} + +func (m *StandardRWMutex) TryLock() bool { + panic("TryLock requires Go 1.18 or later") +} + +func (m *StandardRWMutex) TryRLock() bool { + panic("TryRLock requires Go 1.18 or later") +} + +func (m *StandardRWMutex) RLocker() sync.Locker { + return m.mu.RLocker() +} diff --git a/vendor/github.com/sasha-s/go-deadlock/sync_disable_go118.go b/vendor/github.com/sasha-s/go-deadlock/sync_disable_go118.go new file mode 100644 index 000000000..41c715585 --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/sync_disable_go118.go @@ -0,0 +1,55 @@ +//go:build deadlock_disable && go1.18 + +package deadlock + +import "sync" + +// StandardMutex wraps sync.Mutex with no deadlock detection +type StandardMutex struct { + mu sync.Mutex +} + +func (m *StandardMutex) Lock() { + m.mu.Lock() +} + +func (m *StandardMutex) Unlock() { + m.mu.Unlock() +} + +func (m *StandardMutex) TryLock() bool { + return m.mu.TryLock() +} + +// StandardRWMutex wraps sync.RWMutex with no deadlock detection +type StandardRWMutex struct { + mu sync.RWMutex +} + +func (m *StandardRWMutex) Lock() { + m.mu.Lock() +} + +func (m *StandardRWMutex) Unlock() { + m.mu.Unlock() +} + +func (m *StandardRWMutex) RLock() { + m.mu.RLock() +} + +func (m *StandardRWMutex) RUnlock() { + m.mu.RUnlock() +} + +func (m *StandardRWMutex) TryLock() bool { + return m.mu.TryLock() +} + +func (m *StandardRWMutex) TryRLock() bool { + return m.mu.TryRLock() +} + +func (m *StandardRWMutex) RLocker() sync.Locker { + return m.mu.RLocker() +} diff --git a/vendor/github.com/sasha-s/go-deadlock/sync_mutex_go118.go b/vendor/github.com/sasha-s/go-deadlock/sync_mutex_go118.go new file mode 100644 index 000000000..e934ba464 --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/sync_mutex_go118.go @@ -0,0 +1,65 @@ +//go:build !goexperiment.synctest && !deadlock_synctest && !deadlock_disable && go1.18 +// +build !goexperiment.synctest,!deadlock_synctest,!deadlock_disable,go1.18 + +package deadlock + +import "sync" + +// StandardMutex wraps sync.Mutex +type StandardMutex struct { + mu sync.Mutex +} + +func (m *StandardMutex) Lock() { + m.mu.Lock() +} + +func (m *StandardMutex) Unlock() { + m.mu.Unlock() +} + +func (m *StandardMutex) TryLock() bool { + return m.mu.TryLock() +} + +// StandardRWMutex wraps sync.RWMutex +type StandardRWMutex struct { + mu sync.RWMutex +} + +func (m *StandardRWMutex) Lock() { + m.mu.Lock() +} + +func (m *StandardRWMutex) Unlock() { + m.mu.Unlock() +} + +func (m *StandardRWMutex) RLock() { + m.mu.RLock() +} + +func (m *StandardRWMutex) RUnlock() { + m.mu.RUnlock() +} + +func (m *StandardRWMutex) TryLock() bool { + return m.mu.TryLock() +} + +func (m *StandardRWMutex) TryRLock() bool { + return m.mu.TryRLock() +} + +func (m *StandardRWMutex) RLocker() sync.Locker { + return m.mu.RLocker() +} + +// Default factory functions +func newStandardMutex() MutexImpl { + return &StandardMutex{} +} + +func newStandardRWMutex() RWMutexImpl { + return &StandardRWMutex{} +} \ No newline at end of file diff --git a/vendor/github.com/sasha-s/go-deadlock/sync_mutex_legacy.go b/vendor/github.com/sasha-s/go-deadlock/sync_mutex_legacy.go new file mode 100644 index 000000000..dd055bf72 --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/sync_mutex_legacy.go @@ -0,0 +1,68 @@ +//go:build !goexperiment.synctest && !deadlock_synctest && !deadlock_disable && !go1.18 +// +build !goexperiment.synctest,!deadlock_synctest,!deadlock_disable,!go1.18 + +package deadlock + +import "sync" + +// StandardMutex wraps sync.Mutex +type StandardMutex struct { + mu sync.Mutex +} + +func (m *StandardMutex) Lock() { + m.mu.Lock() +} + +func (m *StandardMutex) Unlock() { + m.mu.Unlock() +} + +func (m *StandardMutex) TryLock() bool { + // TryLock is not available before Go 1.18 + panic("TryLock requires Go 1.18 or later") +} + +// StandardRWMutex wraps sync.RWMutex +type StandardRWMutex struct { + mu sync.RWMutex +} + +func (m *StandardRWMutex) Lock() { + m.mu.Lock() +} + +func (m *StandardRWMutex) Unlock() { + m.mu.Unlock() +} + +func (m *StandardRWMutex) RLock() { + m.mu.RLock() +} + +func (m *StandardRWMutex) RUnlock() { + m.mu.RUnlock() +} + +func (m *StandardRWMutex) TryLock() bool { + // TryLock is not available before Go 1.18 + panic("TryLock requires Go 1.18 or later") +} + +func (m *StandardRWMutex) TryRLock() bool { + // TryRLock is not available before Go 1.18 + panic("TryRLock requires Go 1.18 or later") +} + +func (m *StandardRWMutex) RLocker() sync.Locker { + return m.mu.RLocker() +} + +// Default factory functions +func newStandardMutex() MutexImpl { + return &StandardMutex{} +} + +func newStandardRWMutex() RWMutexImpl { + return &StandardRWMutex{} +} \ No newline at end of file diff --git a/vendor/github.com/sasha-s/go-deadlock/synctest_mutex.go b/vendor/github.com/sasha-s/go-deadlock/synctest_mutex.go new file mode 100644 index 000000000..de628794b --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/synctest_mutex.go @@ -0,0 +1,234 @@ +//go:build (goexperiment.synctest || deadlock_synctest) && !deadlock_disable + +package deadlock + +import ( + "sync" + "sync/atomic" +) + +// ChannelMutex implements MutexImpl using channels for synctest compatibility +type ChannelMutex struct { + ch chan struct{} + locked int32 // atomic + once sync.Once +} + +func (m *ChannelMutex) init() { + m.once.Do(func() { + m.ch = make(chan struct{}, 1) + }) +} + +func (m *ChannelMutex) Lock() { + m.init() + m.ch <- struct{}{} + atomic.StoreInt32(&m.locked, 1) +} + +func (m *ChannelMutex) Unlock() { + if atomic.LoadInt32(&m.locked) == 0 { + panic("unlock of unlocked mutex") + } + atomic.StoreInt32(&m.locked, 0) + <-m.ch +} + +func (m *ChannelMutex) TryLock() bool { + m.init() + select { + case m.ch <- struct{}{}: + atomic.StoreInt32(&m.locked, 1) + return true + default: + return false + } +} + +// ChannelRWMutex implements RWMutexImpl with writer priority using channels. +// Implements writer-priority semantics using the "Third Readers-Writers Problem" solution. +type ChannelRWMutex struct { + resource chan struct{} // The actual resource being protected + readTry chan struct{} // Gate that closes when writers waiting + rmutex chan struct{} // Protects readCount modifications + wmutex chan struct{} // Protects writeCount modifications + readCount int32 // Number of active readers + writeCount int32 // Number of waiting/active writers + once sync.Once +} + +func (m *ChannelRWMutex) init() { + m.once.Do(func() { + m.resource = make(chan struct{}, 1) + m.readTry = make(chan struct{}, 1) + m.rmutex = make(chan struct{}, 1) + m.wmutex = make(chan struct{}, 1) + // Initially, all semaphores are "released" (have a token) + m.resource <- struct{}{} + m.readTry <- struct{}{} + m.rmutex <- struct{}{} + m.wmutex <- struct{}{} + }) +} + +func (m *ChannelRWMutex) Lock() { + m.init() + // Protect writeCount modification + <-m.wmutex + count := atomic.AddInt32(&m.writeCount, 1) + if count == 1 { + // First writer: close the gate to block new readers + <-m.readTry + } + m.wmutex <- struct{}{} // Release wmutex + + // Acquire the resource (wait for existing readers to finish) + <-m.resource +} + +func (m *ChannelRWMutex) Unlock() { + // Release the resource + m.resource <- struct{}{} + + // Protect writeCount modification + <-m.wmutex + count := atomic.AddInt32(&m.writeCount, -1) + if count == 0 { + // Last writer: reopen the gate for readers + m.readTry <- struct{}{} + } + m.wmutex <- struct{}{} +} + +func (m *ChannelRWMutex) RLock() { + m.init() + // Wait at the gate (blocks if writers are waiting) + <-m.readTry + + // Protect readCount modification + <-m.rmutex + count := atomic.AddInt32(&m.readCount, 1) + if count == 1 { + // First reader: acquire the resource to block writers + <-m.resource + } + m.rmutex <- struct{}{} // Release rmutex + + // Release the gate so other readers can pass + m.readTry <- struct{}{} +} + +func (m *ChannelRWMutex) RUnlock() { + <-m.rmutex + count := atomic.AddInt32(&m.readCount, -1) + if count < 0 { + m.rmutex <- struct{}{} + panic("RUnlock of unlocked RWMutex") + } + if count == 0 { + // Last reader: release the resource for writers + m.resource <- struct{}{} + } + m.rmutex <- struct{}{} +} + +func (m *ChannelRWMutex) TryLock() bool { + m.init() + // Try to acquire wmutex + select { + case <-m.wmutex: + default: + return false + } + + count := atomic.AddInt32(&m.writeCount, 1) + if count == 1 { + // First writer: try to close gate + select { + case <-m.readTry: + default: + // Failed, rollback + atomic.AddInt32(&m.writeCount, -1) + m.wmutex <- struct{}{} + return false + } + } + m.wmutex <- struct{}{} + + // Try to acquire resource + select { + case <-m.resource: + return true + default: + // Failed, rollback writer count + <-m.wmutex + count = atomic.AddInt32(&m.writeCount, -1) + if count == 0 { + m.readTry <- struct{}{} + } + m.wmutex <- struct{}{} + return false + } +} + +func (m *ChannelRWMutex) TryRLock() bool { + m.init() + // Try to pass through the gate + select { + case <-m.readTry: + default: + return false + } + + // Try to acquire rmutex + select { + case <-m.rmutex: + default: + // Failed, release gate + m.readTry <- struct{}{} + return false + } + + count := atomic.AddInt32(&m.readCount, 1) + if count == 1 { + // First reader: try to acquire resource + select { + case <-m.resource: + default: + // Failed, rollback + atomic.AddInt32(&m.readCount, -1) + m.rmutex <- struct{}{} + m.readTry <- struct{}{} + return false + } + } + m.rmutex <- struct{}{} + m.readTry <- struct{}{} + return true +} + +func (m *ChannelRWMutex) RLocker() sync.Locker { + return (*channelRLocker)(m) +} + +type channelRLocker ChannelRWMutex + +func (r *channelRLocker) Lock() { (*ChannelRWMutex)(r).RLock() } +func (r *channelRLocker) Unlock() { (*ChannelRWMutex)(r).RUnlock() } + +// Factory functions for synctest +func newChannelMutex() MutexImpl { + return &ChannelMutex{ + ch: make(chan struct{}, 1), + } +} + +func newChannelRWMutex() RWMutexImpl { + m := &ChannelRWMutex{} + m.init() + return m +} + +// Type aliases to override the standard mutex types for synctest +type StandardMutex = ChannelMutex +type StandardRWMutex = ChannelRWMutex diff --git a/vendor/github.com/sasha-s/go-deadlock/timerpool_default.go b/vendor/github.com/sasha-s/go-deadlock/timerpool_default.go new file mode 100644 index 000000000..199a52050 --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/timerpool_default.go @@ -0,0 +1,18 @@ +//go:build !goexperiment.synctest && !deadlock_synctest && !deadlock_disable && !go1.25 + +package deadlock + +// shouldDisableTimerPool determines if timer pooling should be disabled +// In normal builds, timer pooling is enabled by default for performance +func shouldDisableTimerPool() bool { + switch Opts.TimerPool { + case TimerPoolDefault: + return false // Default: enable timer pooling for performance + case TimerPoolEnabled: + return false + case TimerPoolDisabled: + return true + default: + return false + } +} diff --git a/vendor/github.com/sasha-s/go-deadlock/timerpool_disable.go b/vendor/github.com/sasha-s/go-deadlock/timerpool_disable.go new file mode 100644 index 000000000..c4dff21b0 --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/timerpool_disable.go @@ -0,0 +1,9 @@ +//go:build deadlock_disable + +package deadlock + +// shouldDisableTimerPool always returns true when deadlock detection is disabled +// since there's no timer pool or deadlock detection happening anyway +func shouldDisableTimerPool() bool { + return true +} diff --git a/vendor/github.com/sasha-s/go-deadlock/timerpool_go125.go b/vendor/github.com/sasha-s/go-deadlock/timerpool_go125.go new file mode 100644 index 000000000..a35cd3f88 --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/timerpool_go125.go @@ -0,0 +1,18 @@ +//go:build !goexperiment.synctest && !deadlock_synctest && !deadlock_disable && go1.25 + +package deadlock + +// shouldDisableTimerPool determines if timer/entry pooling should be disabled. +// In Go 1.25, pooling is enabled by default for performance. +func shouldDisableTimerPool() bool { + switch Opts.TimerPool { + case TimerPoolDefault: + return false // Default: enable timer pooling for performance + case TimerPoolEnabled: + return false + case TimerPoolDisabled: + return true + default: + return false + } +} diff --git a/vendor/github.com/sasha-s/go-deadlock/timerpool_synctest.go b/vendor/github.com/sasha-s/go-deadlock/timerpool_synctest.go new file mode 100644 index 000000000..35e55ef78 --- /dev/null +++ b/vendor/github.com/sasha-s/go-deadlock/timerpool_synctest.go @@ -0,0 +1,18 @@ +//go:build (goexperiment.synctest || deadlock_synctest) && !deadlock_disable + +package deadlock + +// shouldDisableTimerPool determines if timer pooling should be disabled +// In synctest builds, timer pooling is disabled by default to avoid cross-bubble issues +func shouldDisableTimerPool() bool { + switch Opts.TimerPool { + case TimerPoolDefault: + return true // Default: disable timer pooling for synctest compatibility + case TimerPoolEnabled: + return false + case TimerPoolDisabled: + return true + default: + return true + } +} diff --git a/vendor/github.com/sasha-s/go-deadlock/trylock.go b/vendor/github.com/sasha-s/go-deadlock/trylock.go index e8a6775b4..b29b263f1 100644 --- a/vendor/github.com/sasha-s/go-deadlock/trylock.go +++ b/vendor/github.com/sasha-s/go-deadlock/trylock.go @@ -1,3 +1,4 @@ +//go:build go1.18 // +build go1.18 package deadlock @@ -27,13 +28,16 @@ func trylock(lockFn func() bool, ptr interface{}) bool { if Opts.Disable { return lockFn() } - stack := callers(1) + stack, buf := callers(1) preLock(stack, ptr) ret := lockFn() if ret { - postLock(stack, ptr) + postLock(stack, buf, ptr) } else { + // TryLock failed: the stack won't be stored in stackGID.buf (postLock is + // skipped), so we must release the pooled buffer directly to avoid a leak. + releaseStackBuf(buf) postUnlock(ptr) - } + } return ret } diff --git a/vendor/modules.txt b/vendor/modules.txt index 9b2236fec..6b8d32e60 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -165,7 +165,7 @@ github.com/samber/lo # github.com/sanity-io/litter v1.5.8 ## explicit; go 1.16 github.com/sanity-io/litter -# github.com/sasha-s/go-deadlock v0.3.6 +# github.com/sasha-s/go-deadlock v0.3.9 ## explicit github.com/sasha-s/go-deadlock # github.com/sirupsen/logrus v1.9.3