|
7 | 7 | "io" |
8 | 8 | "os" |
9 | 9 | "sync" |
| 10 | + "sync/atomic" |
10 | 11 | "time" |
11 | 12 |
|
12 | 13 | "github.com/petermattis/goid" |
@@ -197,104 +198,158 @@ func lock(lockFn func(), ptr interface{}) { |
197 | 198 | if Opts.DeadlockTimeout <= 0 { |
198 | 199 | lockFn() |
199 | 200 | } else { |
200 | | - ch := make(chan struct{}) |
201 | 201 | currentID := goid.Get() |
202 | | - go checkDeadlock(stack, ptr, currentID, ch) |
| 202 | + e := dw.register(stack, ptr, currentID) |
203 | 203 | lockFn() |
| 204 | + dw.deregister(e) |
204 | 205 | postLock(stack, ptr) |
205 | | - close(ch) |
206 | 206 | return |
207 | 207 | } |
208 | 208 | postLock(stack, ptr) |
209 | 209 | } |
210 | 210 |
|
211 | | -var timersPool sync.Pool |
| 211 | +// pendingEntry tracks a goroutine that is waiting to acquire a lock. Entries are |
| 212 | +// pooled to avoid per-lock heap allocations (goroutine stacks, channels, closures). |
| 213 | +// |
| 214 | +// Timer safety invariants: |
| 215 | +// - checkFn is allocated once per entry and reused across pool cycles, so recycling |
| 216 | +// an entry does not allocate a new closure. |
| 217 | +// - The done flag synchronizes the callback with deregister: deregister sets done=1 |
| 218 | +// before calling Stop(), and the callback checks done before acting. Because both |
| 219 | +// use atomic operations, the callback is guaranteed to observe done=1 if deregister |
| 220 | +// has already run — even if the runtime already scheduled the callback. |
| 221 | +// - An entry is only returned to the pool when timer.Stop() returns true, meaning |
| 222 | +// the timer was successfully cancelled and the callback will never run. This prevents |
| 223 | +// a recycled entry from being mutated by an in-flight callback. |
| 224 | +// - When Stop() returns false (callback already firing or queued), the entry is |
| 225 | +// intentionally leaked to GC. This only happens in the rare deadlock-timeout path. |
| 226 | +type pendingEntry struct { |
| 227 | + stack []uintptr |
| 228 | + ptr interface{} |
| 229 | + gid int64 |
| 230 | + done int32 // atomic: 0=pending, 1=acquired |
| 231 | + timer *time.Timer |
| 232 | + checkFn func() |
| 233 | +} |
212 | 234 |
|
213 | | -func acquireTimer(d time.Duration) *time.Timer { |
214 | | - if shouldDisableTimerPool() { |
215 | | - return time.NewTimer(Opts.DeadlockTimeout) |
| 235 | +func newPendingEntry() *pendingEntry { |
| 236 | + e := &pendingEntry{} |
| 237 | + // Capture e by pointer so the closure is stable across pool reuse — no new |
| 238 | + // closure allocation when the entry is recycled. |
| 239 | + e.checkFn = func() { |
| 240 | + // If the lock was acquired (done=1), the entry may already be back in the |
| 241 | + // pool or being reused. Bail out unconditionally. |
| 242 | + if atomic.LoadInt32(&e.done) != 0 { |
| 243 | + return |
| 244 | + } |
| 245 | + onDeadlockTimeout(e) |
216 | 246 | } |
| 247 | + return e |
| 248 | +} |
217 | 249 |
|
218 | | - t, ok := timersPool.Get().(*time.Timer) |
219 | | - if ok { |
220 | | - _ = t.Reset(d) |
221 | | - return t |
222 | | - } |
223 | | - return time.NewTimer(Opts.DeadlockTimeout) |
| 250 | +var pendingPool = sync.Pool{ |
| 251 | + New: func() interface{} { |
| 252 | + return newPendingEntry() |
| 253 | + }, |
224 | 254 | } |
225 | 255 |
|
226 | | -func releaseTimer(t *time.Timer) { |
227 | | - stopped := t.Stop() |
| 256 | +type deadlockWatcher struct{} |
228 | 257 |
|
229 | | - // Skip timer pooling if disabled |
| 258 | +var dw deadlockWatcher |
| 259 | + |
| 260 | +func (w *deadlockWatcher) register(stack []uintptr, ptr interface{}, gid int64) *pendingEntry { |
| 261 | + var e *pendingEntry |
230 | 262 | if shouldDisableTimerPool() { |
231 | | - return |
| 263 | + e = newPendingEntry() |
| 264 | + } else { |
| 265 | + e = pendingPool.Get().(*pendingEntry) |
232 | 266 | } |
233 | | - |
234 | | - // Use non-blocking drain to avoid hanging in synctest bubbles. |
235 | | - // Blocking on t.C can deadlock when testing/synctest virtualizes time. |
236 | | - if !stopped { |
237 | | - select { |
238 | | - case <-t.C: |
239 | | - default: |
240 | | - } |
| 267 | + e.stack = stack |
| 268 | + e.ptr = ptr |
| 269 | + e.gid = gid |
| 270 | + atomic.StoreInt32(&e.done, 0) |
| 271 | + if e.timer == nil { |
| 272 | + // First use (freshly allocated entry): create the AfterFunc timer. |
| 273 | + // AfterFunc avoids the channel-drain problems of channel-based timers, |
| 274 | + // which are especially problematic under testing/synctest. |
| 275 | + e.timer = time.AfterFunc(Opts.DeadlockTimeout, e.checkFn) |
| 276 | + } else { |
| 277 | + // Reused from pool: the timer was previously Stop()'d successfully |
| 278 | + // (guaranteed by deregister), so Reset is safe here. |
| 279 | + e.timer.Reset(Opts.DeadlockTimeout) |
241 | 280 | } |
| 281 | + return e |
| 282 | +} |
242 | 283 |
|
243 | | - timersPool.Put(t) |
| 284 | +// deregister marks the lock as acquired and cancels the deadlock timer. |
| 285 | +// Must be called exactly once per register call. The entry pointer is |
| 286 | +// stack-local in lock(), so concurrent or duplicate calls cannot occur. |
| 287 | +func (w *deadlockWatcher) deregister(e *pendingEntry) { |
| 288 | + // Mark done BEFORE stopping the timer. The callback checks done with an |
| 289 | + // atomic load, so even if the timer fires concurrently, the callback will |
| 290 | + // see done=1 and return without acting. |
| 291 | + atomic.StoreInt32(&e.done, 1) |
| 292 | + stopped := e.timer.Stop() |
| 293 | + // Only recycle the entry if Stop() confirmed the callback won't run. |
| 294 | + // If Stop() returned false the callback is already executing or queued; |
| 295 | + // recycling would race with the callback reading entry fields. |
| 296 | + if stopped && !shouldDisableTimerPool() { |
| 297 | + e.stack = nil |
| 298 | + e.ptr = nil |
| 299 | + e.gid = 0 |
| 300 | + pendingPool.Put(e) |
| 301 | + } |
244 | 302 | } |
245 | 303 |
|
246 | | -func checkDeadlock(stack []uintptr, ptr interface{}, currentID int64, ch <-chan struct{}) { |
247 | | - t := acquireTimer(Opts.DeadlockTimeout) |
248 | | - defer releaseTimer(t) |
249 | | - for { |
250 | | - select { |
251 | | - case <-t.C: |
252 | | - lo.mu.Lock() |
253 | | - holders, ok := lo.cur[ptr] |
254 | | - if !ok || len(holders) == 0 { |
255 | | - lo.mu.Unlock() |
256 | | - break // Nobody seems to be holding the lock, try again. |
257 | | - } |
258 | | - Opts.mu.Lock() |
259 | | - fmt.Fprintln(Opts.LogBuf, header) |
260 | | - for _, prev := range holders { |
261 | | - fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed") |
262 | | - fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", prev.gid, ptr) |
263 | | - printStack(Opts.LogBuf, prev.stack) |
264 | | - } |
265 | | - fmt.Fprintln(Opts.LogBuf, "Have been trying to lock it again for more than", Opts.DeadlockTimeout) |
266 | | - fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", currentID, ptr) |
267 | | - printStack(Opts.LogBuf, stack) |
268 | | - stacks := stacks() |
269 | | - grs := bytes.Split(stacks, []byte("\n\n")) |
270 | | - for _, prev := range holders { |
271 | | - for _, g := range grs { |
272 | | - if goid.ExtractGID(g) == prev.gid { |
273 | | - fmt.Fprintln(Opts.LogBuf, "Here is what goroutine", prev.gid, "doing now") |
274 | | - Opts.LogBuf.Write(g) |
275 | | - fmt.Fprintln(Opts.LogBuf) |
276 | | - } |
277 | | - } |
278 | | - } |
279 | | - lo.other(ptr) |
280 | | - if Opts.PrintAllCurrentGoroutines { |
281 | | - fmt.Fprintln(Opts.LogBuf, "All current goroutines:") |
282 | | - Opts.LogBuf.Write(stacks) |
283 | | - } |
284 | | - fmt.Fprintln(Opts.LogBuf) |
285 | | - if buf, ok := Opts.LogBuf.(*bufio.Writer); ok { |
286 | | - buf.Flush() |
| 304 | +func onDeadlockTimeout(e *pendingEntry) { |
| 305 | + lo.mu.Lock() |
| 306 | + holders, ok := lo.cur[e.ptr] |
| 307 | + if !ok || len(holders) == 0 { |
| 308 | + // Lock appears unheld (transient state — holder may have just released). |
| 309 | + // Reschedule if the waiter is still pending. Note: this creates a new timer |
| 310 | + // (e.timer is not updated), so if deregister runs later it will Stop() the |
| 311 | + // original (already-fired) timer, get false, and skip pooling. The new timer's |
| 312 | + // callback will then observe done=1 and no-op. This is safe but means the |
| 313 | + // entry won't be recycled — acceptable since this is the rare timeout path. |
| 314 | + lo.mu.Unlock() |
| 315 | + if atomic.LoadInt32(&e.done) == 0 { |
| 316 | + time.AfterFunc(Opts.DeadlockTimeout, e.checkFn) |
| 317 | + } |
| 318 | + return |
| 319 | + } |
| 320 | + Opts.mu.Lock() |
| 321 | + fmt.Fprintln(Opts.LogBuf, header) |
| 322 | + for _, prev := range holders { |
| 323 | + fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed") |
| 324 | + fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", prev.gid, e.ptr) |
| 325 | + printStack(Opts.LogBuf, prev.stack) |
| 326 | + } |
| 327 | + fmt.Fprintln(Opts.LogBuf, "Have been trying to lock it again for more than", Opts.DeadlockTimeout) |
| 328 | + fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", e.gid, e.ptr) |
| 329 | + printStack(Opts.LogBuf, e.stack) |
| 330 | + stacks := stacks() |
| 331 | + grs := bytes.Split(stacks, []byte("\n\n")) |
| 332 | + for _, prev := range holders { |
| 333 | + for _, g := range grs { |
| 334 | + if goid.ExtractGID(g) == prev.gid { |
| 335 | + fmt.Fprintln(Opts.LogBuf, "Here is what goroutine", prev.gid, "doing now") |
| 336 | + Opts.LogBuf.Write(g) |
| 337 | + fmt.Fprintln(Opts.LogBuf) |
287 | 338 | } |
288 | | - Opts.mu.Unlock() |
289 | | - lo.mu.Unlock() |
290 | | - Opts.OnPotentialDeadlock() |
291 | | - <-ch |
292 | | - return |
293 | | - case <-ch: |
294 | | - return |
295 | 339 | } |
296 | | - t.Reset(Opts.DeadlockTimeout) |
297 | 340 | } |
| 341 | + lo.other(e.ptr) |
| 342 | + if Opts.PrintAllCurrentGoroutines { |
| 343 | + fmt.Fprintln(Opts.LogBuf, "All current goroutines:") |
| 344 | + Opts.LogBuf.Write(stacks) |
| 345 | + } |
| 346 | + fmt.Fprintln(Opts.LogBuf) |
| 347 | + if buf, ok := Opts.LogBuf.(*bufio.Writer); ok { |
| 348 | + buf.Flush() |
| 349 | + } |
| 350 | + Opts.mu.Unlock() |
| 351 | + lo.mu.Unlock() |
| 352 | + Opts.OnPotentialDeadlock() |
298 | 353 | } |
299 | 354 |
|
300 | 355 | type lockOrder struct { |
|
0 commit comments