Files
tailscale/ipn/ipnlocal/watchdog.go
T
Brad FitzpatrickandBrad Fitzpatrick 52400dc6f4 ipn/ipnlocal: add back a watchdog after earlier removal from engine
Commit 2b338dd6a8 removed watchdogEngine because it was weird
(so many methods) and increasingly unnecessary after we'd cleaned up
and simplified so much of the locking.

This adds back a watchdog, but an easier to maintain one that's more
idiomatic.

Updates #19759

Change-Id: I86c458473e126c0809f37696446ce7acf4cc4eb9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-06-02 11:57:12 -07:00

160 lines
4.3 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipnlocal
import (
"log"
"net/netip"
"runtime"
"time"
"tailscale.com/tstime"
)
// deadlockProbeDelay is how long a watched call must be in flight before we
// start probing locks to check for a deadlock. Calls that complete sooner do
// not trigger any probing.
const deadlockProbeDelay = 5 * time.Second
// deadlockTimeout is how long the lock-probing goroutine is allowed to run
// before we declare a deadlock and panic with goroutine stacks. That is, it's
// the maximum total time we allow any of the probed locks to be held.
const deadlockTimeout = 30 * time.Second
// CheckDeadlocks schedules a delayed deadlock probe and returns a function to
// call when the operation being watched is done. Callers typically use it as
// "defer b.CheckDeadlocks()()" to bracket a region of code that should not
// take more than [deadlockProbeDelay].
//
// This is a backstop for detecting and debugging deadlocks in the process, replacing
// the earlier watchdogEngine removed in 2b338dd6a8dbd.
func (b *LocalBackend) CheckDeadlocks() (done func()) {
// Bump the in-flight count. If a watched region is already open, the
// probe timer is already armed, so the bump is all we need to do: the
// matching doneDeadlockCheck will decrement when this caller returns and
// only the last one out will stop the timer.
if b.deadlockChecksInFlight.Add(1) != 1 {
return b.doneDeadlockCheck
}
// Fast path to avoid the deadlockTimerMu+Timer.Reset cost when
// CheckDeadlocks is called many times per second by non-overlapping
// callers: re-arm the probe timer at most once per wall-clock second.
// We use a unix-seconds timestamp (+1 so 0 can mean "never") and a CAS
// so that only one caller per second proceeds to touch the timer; the
// rest return early.
nowUnix := tstime.DefaultClock{Clock: b.Clock()}.Now().Unix() + 1
lastUnix := b.lastDeadlockCheckUnix.Load()
if lastUnix == nowUnix || !b.lastDeadlockCheckUnix.CompareAndSwap(lastUnix, nowUnix) {
return b.doneDeadlockCheck
}
// Slow path: (re)arm the probe timer. Lazily create it on first use.
b.deadlockTimerMu.Lock()
defer b.deadlockTimerMu.Unlock()
t := b.deadlockProbeTimer
if t == nil {
t = time.AfterFunc(deadlockProbeDelay, b.runDeadlockProbe)
b.deadlockProbeTimer = t
} else {
t.Reset(deadlockProbeDelay)
}
return b.doneDeadlockCheck
}
func (b *LocalBackend) doneDeadlockCheck() {
switch n := b.deadlockChecksInFlight.Add(-1); {
case n > 0:
return
case n < 0:
panic("ipnlocal: doneDeadlockCheck called without matching CheckDeadlocks")
}
b.deadlockTimerMu.Lock()
defer b.deadlockTimerMu.Unlock()
if b.deadlockProbeTimer == nil {
return
}
b.deadlockProbeTimer.Stop()
}
func (b *LocalBackend) runDeadlockProbe() {
b.deadlockTimerMu.Lock()
defer b.deadlockTimerMu.Unlock()
if b.deadlockChecksInFlight.Load() == 0 {
return
}
t := b.deadlockTimer
if t == nil {
t = time.AfterFunc(deadlockTimeout, b.reportDeadlock)
b.deadlockTimer = t
} else {
t.Reset(deadlockTimeout)
}
defer t.Stop()
b.probeLocks()
}
func (b *LocalBackend) probeLocks() {
b.probeLocalBackendLock()
sys := b.sys
if sys == nil {
return
}
if bus, ok := sys.Bus.GetOK(); ok && bus != nil {
bus.ProbeLocks()
}
if dialer, ok := sys.Dialer.GetOK(); ok && dialer != nil {
dialer.ProbeLocks()
}
if dm, ok := sys.DNSManager.GetOK(); ok && dm != nil {
dm.ProbeLocks()
}
if e, ok := sys.Engine.GetOK(); ok && e != nil {
e.PeerForIP(netip.Addr{}) // acquires e.mu and e.wgLock
}
if nm, ok := sys.NetMon.GetOK(); ok && nm != nil {
nm.ProbeLocks()
}
if mc, ok := sys.MagicSock.GetOK(); ok && mc != nil {
mc.ProbeLocks()
}
if tun, ok := sys.Tun.GetOK(); ok && tun != nil {
tun.ProbeLocks()
}
if ht, ok := sys.HealthTracker.GetOK(); ok && ht != nil {
ht.ProbeLocks()
}
}
func (b *LocalBackend) probeLocalBackendLock() {
b.mu.Lock()
defer b.mu.Unlock()
}
func (b *LocalBackend) reportDeadlock() {
logf := b.logf
if logf == nil {
logf = log.Printf
}
logf("ipnlocal watchdog goroutine stacks:\n%s", goroutineStacks())
panic("ipnlocal: watchdog timeout")
}
func goroutineStacks() []byte {
buf := make([]byte, 256<<10)
for {
n := runtime.Stack(buf, true)
if n < len(buf) {
return buf[:n]
}
buf = make([]byte, 2*len(buf))
}
}