Skip to main content
Glama
detector.go6.51 kB
// Package deadlock provides deadlock detection for development and debugging package deadlock import ( "fmt" "reflect" "runtime" "sync" "sync/atomic" "time" "unsafe" ) // Detector monitors lock acquisition patterns to detect potential deadlocks type Detector struct { enabled atomic.Bool locks sync.Map // goroutineID -> []lockInfo lockOrder sync.Map // lockAddr -> []lockAddr (what locks were held when acquiring this lock) mu sync.Mutex } type lockInfo struct { addr uintptr file string line int acquiredAt time.Time lockType string // "RLock", "Lock", "Channel" } var globalDetector = &Detector{} // Enable turns on deadlock detection (should only be used in debug/test mode) func Enable() { globalDetector.enabled.Store(true) } // Disable turns off deadlock detection func Disable() { globalDetector.enabled.Store(false) } // BeforeLock should be called before acquiring a lock func BeforeLock(lock interface{}, lockType string) { if !globalDetector.enabled.Load() { return } gid := getGoroutineID() addr := getLockAddr(lock) // Get caller info _, file, line, _ := runtime.Caller(2) info := lockInfo{ addr: addr, file: file, line: line, acquiredAt: time.Now(), lockType: lockType, } // Check for potential deadlock globalDetector.checkDeadlock(gid, addr) // Record lock acquisition globalDetector.recordLock(gid, info) } // AfterUnlock should be called after releasing a lock func AfterUnlock(lock interface{}) { if !globalDetector.enabled.Load() { return } gid := getGoroutineID() addr := getLockAddr(lock) globalDetector.removeLock(gid, addr) } // checkDeadlock checks for potential deadlock scenarios func (d *Detector) checkDeadlock(gid uint64, newLockAddr uintptr) { // Get current locks held by this goroutine if locksVal, ok := d.locks.Load(gid); ok { currentLocks := locksVal.([]lockInfo) // Check if we're trying to acquire a lock we already hold for _, lock := range currentLocks { if lock.addr == newLockAddr { panic(fmt.Sprintf("DEADLOCK: Goroutine %d trying to acquire lock %x it already holds\n"+ "First acquired at: %s:%d\n"+ "Attempting to acquire again at current location", gid, newLockAddr, lock.file, lock.line)) } } // Record lock ordering for _, lock := range currentLocks { d.recordLockOrder(lock.addr, newLockAddr) } } // Check for circular dependencies d.checkCircularDependency(newLockAddr) } // recordLockOrder records that lockA was held when acquiring lockB func (d *Detector) recordLockOrder(lockA, lockB uintptr) { d.mu.Lock() defer d.mu.Unlock() var deps []uintptr if depsVal, ok := d.lockOrder.Load(lockA); ok { deps = depsVal.([]uintptr) } // Check if this ordering already exists for _, dep := range deps { if dep == lockB { return } } deps = append(deps, lockB) d.lockOrder.Store(lockA, deps) } // checkCircularDependency checks if acquiring newLock would create a cycle func (d *Detector) checkCircularDependency(newLock uintptr) { visited := make(map[uintptr]bool) path := make([]uintptr, 0) var checkCycle func(lock uintptr) bool checkCycle = func(lock uintptr) bool { if visited[lock] { // Found a cycle for i, l := range path { if l == lock { cycle := append(path[i:], lock) panic(fmt.Sprintf("DEADLOCK: Circular lock dependency detected: %v", cycle)) } } return false } visited[lock] = true path = append(path, lock) defer func() { path = path[:len(path)-1] }() if depsVal, ok := d.lockOrder.Load(lock); ok { deps := depsVal.([]uintptr) for _, dep := range deps { if dep == newLock { // This would create a cycle cycle := append(path, newLock) panic(fmt.Sprintf("DEADLOCK: Lock order violation would create cycle: %v", cycle)) } checkCycle(dep) } } return false } // Start checking from all locks that depend on newLock d.lockOrder.Range(func(key, value interface{}) bool { lockAddr := key.(uintptr) deps := value.([]uintptr) for _, dep := range deps { if dep == newLock { visited = make(map[uintptr]bool) path = make([]uintptr, 0) checkCycle(lockAddr) } } return true }) } // recordLock records that a goroutine acquired a lock func (d *Detector) recordLock(gid uint64, info lockInfo) { var locks []lockInfo if locksVal, ok := d.locks.Load(gid); ok { locks = locksVal.([]lockInfo) } locks = append(locks, info) d.locks.Store(gid, locks) } // removeLock records that a goroutine released a lock func (d *Detector) removeLock(gid uint64, addr uintptr) { if locksVal, ok := d.locks.Load(gid); ok { locks := locksVal.([]lockInfo) for i, lock := range locks { if lock.addr == addr { // Remove this lock locks = append(locks[:i], locks[i+1:]...) if len(locks) == 0 { d.locks.Delete(gid) } else { d.locks.Store(gid, locks) } return } } } } // getGoroutineID extracts the current goroutine ID func getGoroutineID() uint64 { var buf [64]byte n := runtime.Stack(buf[:], false) // Parse goroutine ID from stack trace // Format: "goroutine 123 [...]" var gid uint64 fmt.Sscanf(string(buf[:n]), "goroutine %d", &gid) return gid } // getLockAddr returns the address of a lock func getLockAddr(lock interface{}) uintptr { switch v := lock.(type) { case *sync.Mutex: return uintptr(unsafe.Pointer(v)) case *sync.RWMutex: return uintptr(unsafe.Pointer(v)) default: // For channels and other types return reflect.ValueOf(lock).Pointer() } } // LockTimeoutDetector monitors for locks held too long type LockTimeoutDetector struct { timeout time.Duration checkers sync.Map // gid -> chan struct{} } // NewLockTimeoutDetector creates a new timeout detector func NewLockTimeoutDetector(timeout time.Duration) *LockTimeoutDetector { return &LockTimeoutDetector{ timeout: timeout, } } // MonitorLock starts monitoring a lock acquisition func (ltd *LockTimeoutDetector) MonitorLock(lock interface{}) func() { if !globalDetector.enabled.Load() { return func() {} } gid := getGoroutineID() done := make(chan struct{}) _, file, line, _ := runtime.Caller(1) ltd.checkers.Store(gid, done) go func() { select { case <-time.After(ltd.timeout): fmt.Printf("WARNING: Lock held for more than %v by goroutine %d at %s:%d\n", ltd.timeout, gid, file, line) case <-done: // Lock was released in time } }() return func() { close(done) ltd.checkers.Delete(gid) } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/standardbeagle/brummer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server