rwmutexplus

package module
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 13, 2024 License: MIT Imports: 13 Imported by: 0

README

RWMutexPlus

An instrumented version of sync.RWMutex with debugging capabilities for Go applications. It should help us spot timing and concurrency issue better.

Author

Christoph C. Cemper

Use cases

  • Find lock contention
  • Identify concurrency where no concurrency should be (set timeout to really low)
  • Indentify abnormally long running actions during exclusive RW locks

Features

  • Monitors lock wait times warns if a lock waits too long or are held for too long
  • Prints warnings when locks are held too long
  • Prints warnings when locks are being waited for too long
  • Provides detailed stack traces when lock contention occurs
  • Tracks lock holders with function names and file locations
  • Filters runtime noise from stack traces for better readability

Current warnings supported

  • LOCK WAIT TIMEOUT WARNING = lock requested, but timeout exceeded
  • LONG LOCK HOLD WARNING = a lock holder holds the lock longer than allowed
  • LOCK ACQUIRED TOO SLOW = a lock requested finally got the lock, but too late
  • LOCK HELD TOO LONG = a slow lock holder finally released the lock

Example Output

When lock contention is detected, you'll see output like this:

=== LOCK WAIT WARNING ===
Waiting for mutex lock for 102.251ms
Caller: main.processData at /app/main.go:42
Last lock holder: main.updateCache at /app/cache.go:156
Last lock time: 101.438125ms ago
Current stack trace:
goroutine 155 [running]:
main.(*processData).Lock()
    /app/main.go:42
main.updateCache()
    /app/cache.go:156
=======================

Installation

go get github.com/christophcemper/rwmutexplus

Usage

package main

import (
    "time"
    "github.com/christophcemper/rwmutexplus"
)

func main() {
    mutex := rwmutexplus.NewInstrumentedRWMutex(100 * time.Millisecond)
    
    // Will trigger warning after 100ms of waiting
    mutex.Lock()
    defer mutex.Unlock()
    
    // Do something...
}

Running Examples

To run the included examples:

go test -v -run=Example

Or take a look at the examples/main.go that demonstrates lock contention with multiple goroutines.

package main

import (
	"fmt"
	"time"

	"github.com/christophcemper/rwmutexplus"
)

func mylocker2(mutex *rwmutexplus.InstrumentedRWMutex) {
	mutex.Lock()
	fmt.Printf("mylocker2 lock acquired\n")
	time.Sleep(500 * time.Millisecond) // Hold lock for 500ms
	mutex.Unlock()
	fmt.Printf("mylocker2 lock released\n")
}

func main() {

	mutex := rwmutexplus.NewInstrumentedRWMutex(100 * time.Millisecond)

	// Create lock contention*
	mylocker1 := func() {
		mutex.Lock()
		fmt.Printf("mylocker1 lock acquired\n")
		time.Sleep(300 * time.Millisecond) // Hold lock for 300ms
		mutex.Unlock()
		fmt.Printf("mylocker1 lock released\n")
	}

	go func(mutex *rwmutexplus.InstrumentedRWMutex) {
		mylocker2(mutex)
	}(mutex)

	time.Sleep(10 * time.Millisecond) // Let goroutine acquire lock

	go func() {
		mylocker1()
	}()

	time.Sleep(10 * time.Millisecond) // Let goroutine acquire lock

	// This will trigger a warning as the goroutine is holding the lock still
	mutex.Lock()
	fmt.Printf("Main Lock acquired\n")
	time.Sleep(50 * time.Millisecond) // pretend  to add some work in critical section
	mutex.Unlock()
	fmt.Printf("Main Lock released\n")

}

The output of the above examples/main.go will be something like this

go run -race example/main.go


mylocker2 lock acquired

=== LONG LOCK HOLD WARNING ===
Still waiting in mutex lock for 101.18725ms
Too long lock holder: main.mylocker2
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:11 main.mylocker2
Last lock time: 101.301375ms ago
========================


=== LOCK WAIT TIMEOUT WARNING ===
Waiting for mutex lock for 100.57475ms
Caller: main.main.func1
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:24 main.main.func1
Last lock holder: main.mylocker2
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:11 main.mylocker2
Last lock time: 111.576667ms ago
Current stack trace:
goroutine 33 [running]:
main.main.func1()
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:24 +0x30
main.main.func3()
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:38 +0x34
created by main.main in goroutine 1
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:37 +0x1f8
========================


=== LOCK WAIT TIMEOUT WARNING ===
Waiting for mutex lock for 100.830541ms
Caller: main.main
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:44 main.main
Last lock holder: main.mylocker2
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:11 main.mylocker2
Last lock time: 122.719584ms ago
Current stack trace:
goroutine 1 [running]:
main.main()
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:44 +0x20c
========================


=== LOCK HELD TOO LONG! ===
Been holding mutex lock for 501.099625ms
Caller: main.mylocker2
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:11 main.mylocker2
Last lock time: 501.2425ms ago
Current stack trace:
goroutine 18 [running]:
main.mylocker2(0xc000060798?)
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:14 +0x7c
main.main.func2(0x0?)
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:32 +0x2c
created by main.main in goroutine 1
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:31 +0x188
========================

mylocker2 lock released

=== LOCK ACQUIRED TOO SLOW ===
Been waiting for mutex lock for 490.646958ms
Caller: main.main.func1
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:24 main.main.func1
Last lock holder: main.mylocker2
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:11 main.mylocker2
Last lock time: 501.533292ms ago
Current stack trace:
goroutine 33 [running]:
main.main.func1()
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:24 +0x30
main.main.func3()
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:38 +0x34
created by main.main in goroutine 1
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:37 +0x1f8
========================

mylocker1 lock acquired

=== LOCK HELD TOO LONG! ===
Been holding mutex lock for 301.126916ms
Caller: main.main.func1
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:24 main.main.func1
Last lock time: 301.201208ms ago
Current stack trace:
goroutine 33 [running]:
main.main.func1()
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:27 +0x80
main.main.func3()
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:38 +0x34
created by main.main in goroutine 1
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:37 +0x1f8
========================

mylocker1 lock released

=== LOCK ACQUIRED TOO SLOW ===
Been waiting for mutex lock for 781.077708ms
Caller: main.main
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:44 main.main
Last lock holder: main.main.func1
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:24 main.main.func1
Last lock time: 301.303916ms ago
Current stack trace:
goroutine 1 [running]:
main.main()
        /Users/christophc/Workspace/common/CaddyLRT/rwmutexplus/examples/main.go:44 +0x20c
========================

Main Lock acquired
Main Lock released

This example demonstrates how

  • mylocker2 is holding the RW lock way too long (500ms)
  • mylocker1 is getting warnings and has to wait of course, but STILL also holds it for too long - 300ms ("LOCK ACQUIRED TOO SLOW", then "LOCK HELD TOO LONG")
  • the poor main function trying to acquired the RW lock has to wait a total of 781ms ("LOCK ACQUIRED TOO SLOW")

License

MIT License. See LICENSE file for details.

Disclaimer

This software is provided "AS IS", without warranty of any kind. The author shall not be liable for any claim, damages or other liability arising from the use of the software.

Documentation

Overview

Package rwmutexplus provides an instrumented version of sync.RWMutex that helps detect potential deadlocks and lock contention issues in Go applications.

Key Features:

  • Monitors lock wait times and emits warnings for potential deadlocks
  • Tracks lock holders with function names and file locations
  • Provides detailed stack traces when lock contention occurs
  • Filters runtime noise from stack traces for better readability

Basic Usage:

mutex := rwmutexplus.NewInstrumentedRWMutex(time.Second)

// Use like a regular RWMutex
mutex.Lock()
// ... critical section ...
mutex.Unlock()

mutex.RLock()
// ... read-only critical section ...
mutex.RUnlock()

The mutex will automatically emit warnings to stdout when locks are held longer than the specified timeout, including information about the current lock holder and stack traces to help diagnose the issue.

Warning Format: When a lock wait timeout occurs, the output includes:

  • Wait duration
  • Caller information (file and line)
  • Last lock holder details
  • Filtered stack trace focusing on application code

This package is particularly useful during development and testing to identify potential deadlocks and performance issues related to lock contention.

registry.go to keep track of all the RWMutexPlus instances

Example (LockContention)
package main

import (
	"fmt"
	"time"

	"github.com/christophcemper/rwmutexplus"
)

func main() {
	mutex := rwmutexplus.NewInstrumentedRWMutex(100 * time.Millisecond)

	fmt.Println("Starting lock contention example...")

	// Create lock contention by holding the lock in a goroutine
	go func() {
		mutex.Lock()
		fmt.Println("Goroutine acquired lock, holding for 200ms...")
		time.Sleep(200 * time.Millisecond)
		mutex.Unlock()
		fmt.Println("Goroutine released lock")
	}()

	// Wait for goroutine to acquire lock
	time.Sleep(10 * time.Millisecond)

	// This will trigger a warning after 100ms
	fmt.Println("Main thread attempting to acquire lock...")
	mutex.Lock()
	fmt.Println("Main thread acquired lock")
	mutex.Unlock()
	fmt.Println("Main thread released lock")

}
Output:

Starting lock contention example...
Goroutine acquired lock, holding for 200ms...
Main thread attempting to acquire lock...
=== LOCK WAIT WARNING ===
Waiting for mutex lock for ...
Last lock holder: ...
========================
Goroutine released lock
Main thread acquired lock
Main thread released lock
Example (ReadLockContention)
package main

import (
	"fmt"
	"time"

	"github.com/christophcemper/rwmutexplus"
)

func main() {
	mutex := rwmutexplus.NewInstrumentedRWMutex(100 * time.Millisecond)

	fmt.Println("Starting read lock contention example...")

	// Hold write lock
	go func() {
		mutex.Lock()
		fmt.Println("Write lock acquired, holding for 200ms...")
		time.Sleep(200 * time.Millisecond)
		mutex.Unlock()
		fmt.Println("Write lock released")
	}()

	// Wait for write lock to be acquired
	time.Sleep(10 * time.Millisecond)

	// This will trigger a warning
	fmt.Println("Attempting to acquire read lock...")
	mutex.RLock()
	fmt.Println("Read lock acquired")
	mutex.RUnlock()
	fmt.Println("Read lock released")

}
Output:

Starting read lock contention example...
Write lock acquired, holding for 200ms...
Attempting to acquire read lock...
Write lock released
Read lock acquired
Read lock released

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func CleanupGoroutineIDs

func CleanupGoroutineIDs()

CleanupGoroutineIDs safely removes unused IDs while preserving active ones Uses a complete map replacement strategy to prevent memory leaks

func DumpAllLockInfo

func DumpAllLockInfo(filters ...LockFilter) string

DumpAllLockInfo prints detailed information about all RWMutexPlus instances filters parameter controls which lock types to show

func ForceCleanup

func ForceCleanup()

ForceCleanup removes all IDs for testing/reset purposes Creates entirely new maps to ensure complete cleanup

func GetDurationEnvOrDefault added in v0.0.5

func GetDurationEnvOrDefault(key string, defaultValue time.Duration) time.Duration

GetDurationEnvOrDefault returns the duration value of an environment variable or the default value if not set

func GetGoroutineID

func GetGoroutineID() uint64

GetGoroutineID returns a unique identifier for the current goroutine Uses a two-phase locking strategy to optimize for the common case where the ID already exists

func GetIntEnvOrDefault added in v0.0.5

func GetIntEnvOrDefault(key string, defaultValue int) int

GetIntEnvOrDefault returns the integer value of an environment variable or the default value if not set

func GetLockInfo added in v0.0.5

func GetLockInfo(li LockInfo) string

func GetStoredGoroutineCount

func GetStoredGoroutineCount() int

GetStoredGoroutineCount returns number of stored IDs for monitoring

func ParseDuration added in v0.0.5

func ParseDuration(s string) (time.Duration, error)

func PrintLockInfo added in v0.0.5

func PrintLockInfo(li LockInfo)

PrintLockInfo prints the lock info from LockInfo interface

func PrintOnce added in v0.0.5

func PrintOnce(msg string) bool

PrintOnce prints a message only once during the process lifetime Returns true if the message was printed, false if it was already printed before

func PrintOncef added in v0.0.5

func PrintOncef(format string, args ...interface{}) bool

PrintOncef formats and prints a message only once during the process lifetime Returns true if the message was printed, false if it was already printed before

func ReleaseGoroutineID

func ReleaseGoroutineID()

ReleaseGoroutineID marks an ID as no longer in active use This allows the cleanup routine to eventually remove it

func ResetPrintOnce added in v0.0.5

func ResetPrintOnce()

ResetPrintOnce clears all tracked messages (mainly for testing)

Types

type ActiveLock

type ActiveLock struct {
	// contains filtered or unexported fields
}

ActiveLock tracks an acquired lock

func (*ActiveLock) GetCallerInfo added in v0.0.5

func (lr *ActiveLock) GetCallerInfo() string

implements LockInfo

func (*ActiveLock) GetGoroutineID added in v0.0.5

func (al *ActiveLock) GetGoroutineID() uint64

implements LockInfo

func (*ActiveLock) GetLockTX added in v0.0.5

func (al *ActiveLock) GetLockTX() LockTX

implements LockInfo

func (*ActiveLock) GetLockType added in v0.0.5

func (al *ActiveLock) GetLockType() LockType

implements LockInfo

func (*ActiveLock) GetPosition added in v0.0.5

func (lr *ActiveLock) GetPosition() string

implements LockInfo

func (*ActiveLock) GetPurpose added in v0.0.5

func (al *ActiveLock) GetPurpose() string

implements LockInfo

func (*ActiveLock) GetSinceTime added in v0.0.5

func (al *ActiveLock) GetSinceTime() time.Duration

implements LockInfo

func (*ActiveLock) String added in v0.0.5

func (al *ActiveLock) String() string

implements LockInfo

type InstrumentedRWMutex

type InstrumentedRWMutex struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

InstrumentedRWMutex wraps sync.RWMutex with debugging capabilities. It tracks lock holders, wait times, and can emit warnings when locks are held for too long.

Example

Example of concurrent usage

mutex := NewInstrumentedRWMutex(100 * time.Millisecond)
data := make(map[string]int)

// Writer
go func() {
	mutex.Lock()
	data["key"] = 42
	time.Sleep(200 * time.Millisecond) // Simulate work
	mutex.Unlock()
}()

// Reader
go func() {
	mutex.RLock()
	_ = data["key"]
	mutex.RUnlock()
}()

// Let the example run
time.Sleep(3000 * time.Millisecond)

func NewInstrumentedRWMutex

func NewInstrumentedRWMutex(lockWaitTimeout time.Duration) *InstrumentedRWMutex

NewInstrumentedRWMutex creates a new InstrumentedRWMutex with the specified lock wait timeout. The timeout determines how long to wait before emitting a warning about potential deadlocks.

Example:

mutex := NewInstrumentedRWMutex(time.Second) // Warn after 1 second of waiting

func (*InstrumentedRWMutex) Lock

func (m *InstrumentedRWMutex) Lock()

Lock acquires an exclusive lock and monitors the wait time.

func (*InstrumentedRWMutex) LockPurpose

func (m *InstrumentedRWMutex) LockPurpose(purpose string)

LockPurpose acquires an exclusive lock and sets the purpose of the mutex and monitors the wait time.

func (*InstrumentedRWMutex) RLock

func (m *InstrumentedRWMutex) RLock()

RLock acquires a shared read lock and monitors the wait time. Similar to Lock, it will emit warnings if the wait time exceeds lockWaitTimeout.

func (*InstrumentedRWMutex) Unlock

func (m *InstrumentedRWMutex) Unlock()

Unlock releases an exclusive lock. if the lock was held for too long, it prints a warning

type LockFilter

type LockFilter uint8
const (
	ShowPendingReads LockFilter = 1 << iota
	ShowPendingWrites
	ShowActiveReads
	ShowActiveWrites
)

type LockInfo added in v0.0.5

type LockInfo interface {
	GetLockTX() LockTX
	GetLockType() LockType
	GetPurpose() string
	GetGoroutineID() uint64
	GetCallerInfo() string
	GetSinceTime() time.Duration
	GetPosition() string
	String() string
}

LockInfo is an interface for both LockRequest and ActiveLock to store lockType, purpose, goRoutineID and callerInfo

type LockRequest

type LockRequest struct {
	// contains filtered or unexported fields
}

LockRequest implements LockInfo

func (*LockRequest) GetCallerInfo added in v0.0.5

func (lr *LockRequest) GetCallerInfo() string

implements LockInfo

func (*LockRequest) GetGoroutineID added in v0.0.5

func (lr *LockRequest) GetGoroutineID() uint64

implements LockInfo

func (*LockRequest) GetLockTX added in v0.0.5

func (lr *LockRequest) GetLockTX() LockTX

implements LockInfo

func (*LockRequest) GetLockType added in v0.0.5

func (lr *LockRequest) GetLockType() LockType

implements LockInfo

func (*LockRequest) GetPosition added in v0.0.5

func (lr *LockRequest) GetPosition() string

implements LockInfo

func (*LockRequest) GetPurpose added in v0.0.5

func (lr *LockRequest) GetPurpose() string

implements LockInfo

func (*LockRequest) GetSinceTime added in v0.0.5

func (lr *LockRequest) GetSinceTime() time.Duration

implements LockInfo

func (*LockRequest) String added in v0.0.5

func (lr *LockRequest) String() string

implements LockInfo

type LockStats

type LockStats struct {
	// contains filtered or unexported fields
}

LockStats tracks statistics for a specific lock purpose

type LockTX added in v0.0.5

type LockTX int

LockTX is the transaction type for the lock object (LockRequest or ActiveLock)

const (
	IsLockRequest LockTX = iota
	IsActiveLock
)

func (LockTX) String added in v0.0.5

func (lo LockTX) String() string

stringer for LockTX

type LockType added in v0.0.5

type LockType int

LockType is the type of lock

const (
	ReadLock LockType = iota
	WriteLock
)

func (LockType) String added in v0.0.5

func (lt LockType) String() string

stringer for LockType

type RWMutexPlus

type RWMutexPlus struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

RWMutexPlus provides a robust logging RWMutex implementation

func NewRWMutexPlus

func NewRWMutexPlus(name string, warningTimeout time.Duration, logger *log.Logger) *RWMutexPlus

func (*RWMutexPlus) Close

func (rw *RWMutexPlus) Close()

Add Close method to RWMutexPlus struct

func (*RWMutexPlus) DebugPrint added in v0.0.5

func (rw *RWMutexPlus) DebugPrint(str string, level ...int)

DebugPrint prints the string if debug level is greater than 0 if variadic/optional param level is provided, it will print only if level is greater than or equal to debugLevel

func (*RWMutexPlus) DebugPrintAllLockInfo added in v0.0.5

func (rw *RWMutexPlus) DebugPrintAllLockInfo(pos string)

func (*RWMutexPlus) GetActiveLocksInfo

func (rw *RWMutexPlus) GetActiveLocksInfo() map[string][]map[string]interface{}

GetActiveLocksInfo returns information about all currently held locks

func (*RWMutexPlus) GetPendingLocksInfo

func (rw *RWMutexPlus) GetPendingLocksInfo() map[string][]map[string]interface{}

GetPendingLocksInfo returns information about locks waiting to be acquired

func (*RWMutexPlus) GetPurposeStats

func (rw *RWMutexPlus) GetPurposeStats() map[string]map[string]int64

GetPurposeStats returns statistics for all lock purposes

func (*RWMutexPlus) Lock

func (rw *RWMutexPlus) Lock()

Add these methods to support backward compatibility

func (*RWMutexPlus) LockWithPurpose

func (rw *RWMutexPlus) LockWithPurpose(purpose string)

func (*RWMutexPlus) OverrideCallerInfoLinesFromEnv added in v0.0.5

func (rw *RWMutexPlus) OverrideCallerInfoLinesFromEnv() bool

func (*RWMutexPlus) OverrideCallerInfoSkipFromEnv added in v0.0.5

func (rw *RWMutexPlus) OverrideCallerInfoSkipFromEnv() bool

func (*RWMutexPlus) OverrideDebugLevelFromEnv added in v0.0.5

func (rw *RWMutexPlus) OverrideDebugLevelFromEnv() bool

func (*RWMutexPlus) OverrideVerboseLevelFromEnv added in v0.0.5

func (rw *RWMutexPlus) OverrideVerboseLevelFromEnv() bool

check os.Getenv variables RWMUTEXTPLUS_VERBOSELEVEL and RWMUTEXTPLUS_DEBUGLEVEL to set the verboseLevel and debugLevel and override any defaults from the constructor code or With...Level setters

func (*RWMutexPlus) OverrideWarningTimeoutFromEnv added in v0.0.5

func (rw *RWMutexPlus) OverrideWarningTimeoutFromEnv() bool

func (*RWMutexPlus) PrintLockInfo

func (m *RWMutexPlus) PrintLockInfo(pos string)

PrintLockInfo prints detailed information about the current state of the mutex including active locks, pending locks, and statistics for each purpose. pos is a string identifier to help track where the print was called from.

func (*RWMutexPlus) PrintOncef added in v0.0.5

func (rw *RWMutexPlus) PrintOncef(format string, args ...interface{})

PrintOncef prints a message if it hasn't been printed before

func (*RWMutexPlus) RLock

func (rw *RWMutexPlus) RLock()

func (*RWMutexPlus) RLockWithPurpose

func (rw *RWMutexPlus) RLockWithPurpose(purpose string)

func (*RWMutexPlus) RUnlock

func (rw *RWMutexPlus) RUnlock()

func (*RWMutexPlus) Unlock

func (rw *RWMutexPlus) Unlock()

Fix the Unlock method to properly handle the activeLock check

func (*RWMutexPlus) WithCallerInfoLines added in v0.0.5

func (rw *RWMutexPlus) WithCallerInfoLines(lines int) *RWMutexPlus

func (*RWMutexPlus) WithDebugLevel

func (rw *RWMutexPlus) WithDebugLevel(level int) *RWMutexPlus

WithDebugLevel sets the debug level (0-1) for the mutex and returns the mutex for chaining

func (*RWMutexPlus) WithVerboseLevel

func (rw *RWMutexPlus) WithVerboseLevel(level int) *RWMutexPlus

WithVerboseLevel sets the verbose level (0-3) for the mutex and returns the mutex for chaining

Directories

Path Synopsis
examples
ex1
ex2
ex3

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL